Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const state = { bases: [], requests: [] };
|
||||
|
||||
function headers(json = false) {
|
||||
const value = {};
|
||||
if (json) value["Content-Type"] = "application/json";
|
||||
return value;
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(path, { ...options, headers: { ...headers(Boolean(options.body)), ...(options.headers || {}) } });
|
||||
let data = {};
|
||||
try { data = await response.json(); } catch (_) { /* empty response */ }
|
||||
if (!response.ok) throw new Error(data.message || data.error || `HTTP ${response.status}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
function notice(message = "", error = false) {
|
||||
$("notice").hidden = !message;
|
||||
$("notice").textContent = message;
|
||||
$("notice").className = `notice${error ? " error" : ""}`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
$("bases").replaceChildren(...state.bases.map((base) => {
|
||||
const row = document.createElement("tr");
|
||||
const cells = [base.base_id, base.server, base.database, base.user];
|
||||
cells.forEach((value) => { const td = document.createElement("td"); td.textContent = value; row.append(td); });
|
||||
const password = document.createElement("td");
|
||||
password.innerHTML = `<span class="badge ${base.has_password ? "" : "missing"}">${base.password_env ? "ENV · " + escapeHtml(base.password_env) : base.has_password ? "Сохранён" : "Не задан"}</span>`;
|
||||
row.append(password);
|
||||
const actions = document.createElement("td"); actions.className = "actions";
|
||||
const edit = document.createElement("button"); edit.className = "text-button"; edit.textContent = "Изменить"; edit.onclick = () => openEditor(base);
|
||||
const remove = document.createElement("button"); remove.className = "text-button danger"; remove.textContent = "Удалить"; remove.onclick = () => deleteBase(base);
|
||||
actions.append(edit, remove); row.append(actions); return row;
|
||||
}));
|
||||
$("empty").hidden = state.bases.length > 0;
|
||||
$("count").textContent = `${state.bases.length} ${state.bases.length === 1 ? "подключение" : "подключений"}`;
|
||||
}
|
||||
|
||||
function renderRequests() {
|
||||
$("requests").replaceChildren(...state.requests.map((item) => {
|
||||
const row = document.createElement("tr");
|
||||
const values = [item.request_id, item.base_id, item.status, (item.objects || []).join(", "), item.created_at ? new Date(item.created_at * 1000).toLocaleString("ru-RU") : "—"];
|
||||
values.forEach((value, index) => { const td = document.createElement("td"); td.textContent = value || "—"; if (index === 2) td.className = `request-status ${item.status || ""}`; if (index === 3) td.className = "request-objects"; row.append(td); });
|
||||
return row;
|
||||
}));
|
||||
$("requestsEmpty").hidden = state.requests.length > 0;
|
||||
$("requestCount").textContent = `${state.requests.length} заявок`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
const div = document.createElement("div"); div.textContent = value; return div.innerHTML;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
notice();
|
||||
const [data, repository] = await Promise.all([request("/admin/api/bases"), request("/admin/api/repository/requests")]);
|
||||
state.bases = data.bases || [];
|
||||
state.requests = repository.requests || [];
|
||||
$("statusDot").classList.add("online"); $("connectionText").textContent = "Адаптер подключён";
|
||||
render();
|
||||
renderRequests();
|
||||
} catch (error) {
|
||||
$("statusDot").classList.remove("online"); $("connectionText").textContent = "Требуется подключение";
|
||||
notice(error.message === "bearer_token_required" ? "Введите Bearer-токен адаптера." : error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function openEditor(base = null) {
|
||||
$("editorTitle").textContent = base ? "Редактировать базу" : "Новая база";
|
||||
$("originalId").value = base?.base_id || "";
|
||||
$("baseId").value = base?.base_id || ""; $("server").value = base?.server || "";
|
||||
$("database").value = base?.database || ""; $("user").value = base?.user || "";
|
||||
$("password").value = ""; $("passwordEnv").value = base?.password_env || "";
|
||||
$("repositoryEnabled").checked = Boolean(base?.repository);
|
||||
$("repositoryBackend").value = base?.repository?.backend || "direct";
|
||||
$("repositoryLayer").value = base?.repository?.layer || "base";
|
||||
$("repositoryLockMode").value = base?.repository?.lock_mode || "automatic";
|
||||
$("repositoryBridgeId").value = base?.repository?.bridge_id || "";
|
||||
$("repositoryUser").value = base?.repository?.repository_user || "";
|
||||
$("passwordHint").textContent = base?.has_password ? "Пароль уже задан. Оставьте пустым, чтобы сохранить текущий." : "Задайте пароль или переменную окружения.";
|
||||
$("editor").showModal(); setTimeout(() => $("baseId").focus(), 30);
|
||||
}
|
||||
|
||||
async function save(event) {
|
||||
event.preventDefault();
|
||||
if (!$("baseForm").reportValidity()) return;
|
||||
const original = $("originalId").value;
|
||||
const payload = { base_id: $("baseId").value.trim(), server: $("server").value.trim(), database: $("database").value.trim(), user: $("user").value.trim(), password: $("password").value, password_env: $("passwordEnv").value.trim(), repository: $("repositoryEnabled").checked ? { backend: $("repositoryBackend").value, layer: $("repositoryLayer").value, lock_mode: $("repositoryLockMode").value, bridge_id: $("repositoryBridgeId").value.trim(), repository_user: $("repositoryUser").value.trim() } : { enabled: false } };
|
||||
$("saveButton").disabled = true;
|
||||
try {
|
||||
await request(original ? `/admin/api/bases/${encodeURIComponent(original)}` : "/admin/api/bases", { method: original ? "PUT" : "POST", body: JSON.stringify(payload) });
|
||||
$("editor").close(); await load(); notice(original ? "Подключение обновлено." : "Подключение добавлено.");
|
||||
} catch (error) { notice(error.message, true); } finally { $("saveButton").disabled = false; }
|
||||
}
|
||||
|
||||
async function deleteBase(base) {
|
||||
if (!confirm(`Удалить подключение «${base.base_id}»?`)) return;
|
||||
try { await request(`/admin/api/bases/${encodeURIComponent(base.base_id)}`, { method: "DELETE" }); await load(); notice("Подключение удалено."); }
|
||||
catch (error) { notice(error.message, true); }
|
||||
}
|
||||
|
||||
$("refreshButton").onclick = load; $("addButton").onclick = () => openEditor(); $("baseForm").onsubmit = save;
|
||||
load();
|
||||
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>1С Adapter · SQL-базы</title>
|
||||
<link rel="stylesheet" href="/admin/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div><span class="mark">1C</span><strong>Adapter Control</strong></div>
|
||||
<div class="connection"><span id="statusDot" class="dot"></span><span id="connectionText">Не подключено</span></div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="intro">
|
||||
<div><p class="eyebrow">Подключения</p><h1>SQL-базы 1С</h1><p>Управление адресами и учётными данными адаптера.</p></div>
|
||||
<button id="addButton" class="primary">+ Добавить базу</button>
|
||||
</section>
|
||||
<section class="panel table-panel">
|
||||
<div class="panel-head"><div><h2>Настроенные базы</h2><span id="count">0 подключений</span></div><button id="refreshButton" class="icon" title="Обновить" aria-label="Обновить">↻</button></div>
|
||||
<div id="notice" class="notice" hidden></div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>Base ID</th><th>SQL Server</th><th>База данных</th><th>Логин</th><th>Пароль</th><th></th></tr></thead><tbody id="bases"></tbody></table>
|
||||
<div id="empty" class="empty">Подключения ещё не настроены.</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel table-panel requests-panel">
|
||||
<div class="panel-head"><div><h2>Заявки на захват</h2><span id="requestCount">0 заявок</span></div></div>
|
||||
<div class="table-wrap">
|
||||
<table><thead><tr><th>Заявка</th><th>База</th><th>Статус</th><th>Объекты</th><th>Создана</th></tr></thead><tbody id="requests"></tbody></table>
|
||||
<div id="requestsEmpty" class="empty">Заявок пока нет.</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<dialog id="editor">
|
||||
<form id="baseForm" method="dialog">
|
||||
<div class="dialog-head"><div><p class="eyebrow">SQL-подключение</p><h2 id="editorTitle">Новая база</h2></div><button value="cancel" class="close" aria-label="Закрыть">×</button></div>
|
||||
<input id="originalId" type="hidden">
|
||||
<div class="grid">
|
||||
<label>Base ID<input id="baseId" required pattern="[A-Za-z0-9_.-]+" placeholder="upo_test"></label>
|
||||
<label>SQL Server / IP<input id="server" required placeholder="192.168.1.10"></label>
|
||||
<label>Имя базы SQL<input id="database" required placeholder="upo_test"></label>
|
||||
<label>SQL-логин<input id="user" required autocomplete="username" placeholder="onec_reader"></label>
|
||||
<label class="wide">Пароль<input id="password" type="password" autocomplete="new-password" placeholder="Оставьте пустым, чтобы не менять"><small id="passwordHint">Пароль сохраняется в защищённом runtime-файле и никогда не отображается.</small></label>
|
||||
<label class="wide">Или переменная окружения<input id="passwordEnv" placeholder="ONEC_SQL_PASSWORD_UPO_TEST"><small>Если заполнено, имеет приоритет над введённым паролем.</small></label>
|
||||
<label class="wide"><input id="repositoryEnabled" type="checkbox"> Конфигурация подключена к хранилищу</label>
|
||||
<label>Доступ к хранилищу<select id="repositoryBackend"><option value="direct">Прямой</option><option value="karman_bridge">Через Карман</option></select></label>
|
||||
<label>Слой<select id="repositoryLayer"><option value="base">Основная конфигурация</option><option value="extension">Расширение</option></select></label>
|
||||
<label>Захват объектов<select id="repositoryLockMode"><option value="manual">Вручную пользователем (SQL-only)</option><option value="automatic" disabled>Через внешнюю 1С (следующая версия)</option></select></label>
|
||||
<label>ID моста (из настройки)<input id="repositoryBridgeId" placeholder="Необязательно"></label>
|
||||
<label>Пользователь хранилища<input id="repositoryUser" autocomplete="off" placeholder="Например, adm"></label>
|
||||
</div>
|
||||
<div class="dialog-actions"><button value="cancel" class="secondary">Отмена</button><button id="saveButton" value="default" class="primary">Сохранить</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<script src="/admin/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
:root{--ink:#17211b;--muted:#68736c;--line:#d7ddd8;--paper:#f3f4f0;--surface:#fff;--accent:#b8dc2e;--accent-dark:#27380c;--danger:#b33a2e;--shadow:0 18px 50px rgba(30,42,34,.11);font-family:"Segoe UI Variable","Aptos",sans-serif;color:var(--ink);background:var(--paper)}*{box-sizing:border-box}body{margin:0}.topbar{height:58px;padding:0 max(24px,calc((100vw - 1180px)/2));display:flex;align-items:center;justify-content:space-between;background:#18211c;color:#f7faf6}.topbar>div{display:flex;align-items:center;gap:10px}.mark{display:grid;place-items:center;width:30px;height:30px;background:var(--accent);color:#17210b;font-weight:900;border-radius:7px}.connection{font-size:13px;color:#bdc6bf}.dot{width:8px;height:8px;border-radius:50%;background:#78827b}.dot.online{background:var(--accent);box-shadow:0 0 0 4px rgba(184,220,46,.12)}main{max-width:1180px;margin:auto;padding:42px 24px 70px}.intro{display:flex;align-items:end;justify-content:space-between;margin-bottom:26px}.eyebrow{margin:0 0 6px;text-transform:uppercase;letter-spacing:.13em;font-size:11px;font-weight:800;color:#71804f}.intro h1{font-family:Georgia,serif;font-size:43px;line-height:1;margin:0}.intro p:not(.eyebrow){color:var(--muted);margin:12px 0 0}.panel{background:var(--surface);border:1px solid var(--line);box-shadow:0 2px 0 rgba(23,33,27,.03)}.auth{display:grid;grid-template-columns:minmax(260px,1fr) auto;gap:12px;align-items:end;padding:18px;margin-bottom:18px}.auth small{grid-column:1/-1}.auth label,.grid label{display:grid;gap:7px;font-size:12px;font-weight:700}.auth input,.grid input{width:100%;border:1px solid #bfc8c1;background:#fbfcfa;border-radius:5px;padding:11px 12px;font:inherit;color:var(--ink);outline:none}.auth input:focus,.grid input:focus{border-color:#6f861c;box-shadow:0 0 0 3px rgba(184,220,46,.18)}button{font:inherit;cursor:pointer}.primary,.secondary,.icon,.close{border:0;border-radius:5px;min-height:40px;padding:0 16px;font-weight:750}.primary{background:var(--accent);color:var(--accent-dark)}.primary:hover{filter:brightness(.94)}.secondary{background:#edf0ec;color:#344038}.table-panel{overflow:hidden}.panel-head{padding:20px 22px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}.panel-head h2{font-size:17px;margin:0 0 3px}.panel-head span{font-size:12px;color:var(--muted)}.icon,.close{font-size:22px;background:transparent;padding:0 10px}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;min-width:820px}th,td{text-align:left;padding:14px 18px;border-bottom:1px solid #e5e9e6;font-size:13px}th{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:#728078;background:#fafbf9}td:first-child{font-family:Consolas,monospace;font-weight:700}.actions{display:flex;justify-content:flex-end;gap:6px}.text-button{border:0;background:transparent;padding:5px;color:#516058}.text-button:hover{color:#17211b}.text-button.danger:hover{color:var(--danger)}.badge{display:inline-flex;align-items:center;gap:6px;color:#3c4a40}.badge:before{content:"";width:7px;height:7px;border-radius:50%;background:var(--accent)}.badge.missing:before{background:#d2a342}.empty{text-align:center;padding:55px;color:var(--muted)}.notice{margin:14px 18px 0;padding:11px 13px;border-left:3px solid #d2a342;background:#fff8df;font-size:13px}.notice.error{border-color:var(--danger);background:#fff0ee;color:#78271f}dialog{border:0;border-radius:8px;padding:0;width:min(660px,calc(100vw - 30px));box-shadow:var(--shadow)}dialog::backdrop{background:rgba(15,23,18,.58);backdrop-filter:blur(2px)}dialog form{padding:24px}.dialog-head{display:flex;justify-content:space-between}.dialog-head h2{font-family:Georgia,serif;font-size:28px;margin:0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:17px;margin:25px 0}.grid .wide{grid-column:1/-1}.grid small,.auth small{color:var(--muted);font-weight:400}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;border-top:1px solid var(--line);padding-top:18px}@media(max-width:700px){main{padding:28px 14px}.intro{align-items:start;gap:20px}.intro h1{font-size:34px}.auth{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.grid .wide{grid-column:auto}.topbar{padding:0 14px}.connection span:last-child{display:none}}
|
||||
Reference in New Issue
Block a user