@@ -484,6 +525,60 @@ function renderSidePanel(state, activeTableMeta) {
`;
}
+function renderCreateTableModal(state) {
+ if (!state.isCreateTableModalOpen) {
+ return "";
+ }
+
+ return `
+
+ `;
+}
+
function renderSortOptions(state) {
const columns = state.tableDetails.columns || [];
@@ -564,6 +659,20 @@ function renderRowsTable(rows, options = {}) {
`;
}
+function applyColumnFilters(rows, columnFilters) {
+ if (!Object.keys(columnFilters).length) {
+ return rows;
+ }
+
+ return rows.filter((row) =>
+ Object.entries(columnFilters).every(([column, value]) =>
+ String(row[column] ?? "")
+ .toLowerCase()
+ .includes(String(value).toLowerCase())
+ )
+ );
+}
+
function formatCell(value) {
if (value === null || value === undefined) {
return `
null`;
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 1be35b6..d29780a 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -27,7 +27,11 @@ const state = {
sortDirection: "asc",
pageNumber: 1,
pageSize: 25,
- selectedRow: null
+ selectedRow: null,
+ columnFilters: {},
+ showColumnFilters: false,
+ isCreateTableModalOpen: false,
+ newTableColumns: [{ id: 1, name: "", type: "varchar(255)", nullable: true, primaryKey: false }]
};
const app = document.querySelector("#app");
@@ -112,6 +116,7 @@ function bindEvents() {
document.querySelector("#addColumnForm")?.addEventListener("submit", wrapAction(handleAddColumn));
document.querySelector("#alterColumnForm")?.addEventListener("submit", wrapAction(handleAlterColumn));
document.querySelector("#createIndexForm")?.addEventListener("submit", wrapAction(handleCreateIndex));
+ document.querySelector("#createTableForm")?.addEventListener("submit", wrapAction(handleCreateTable));
document.querySelectorAll("[data-page]").forEach((element) => {
element.addEventListener(
@@ -149,6 +154,10 @@ function bindEvents() {
document.querySelectorAll("[data-action]").forEach((element) => {
element.addEventListener("click", wrapAction(handleAction));
});
+
+ document.querySelectorAll("[data-input-action='set-column-filter']").forEach((element) => {
+ element.addEventListener("input", wrapAction(handleAction));
+ });
}
function wrapAction(handler) {
@@ -209,6 +218,11 @@ async function handleAction(event) {
return;
}
+ if (action === "toggle-column-filters") {
+ state.showColumnFilters = !state.showColumnFilters;
+ return;
+ }
+
if (action === "run-sql") {
state.sqlDraft = document.querySelector("#sqlEditor")?.value || "";
const result = await api.executeSql(state.sqlDraft);
@@ -287,6 +301,62 @@ async function handleAction(event) {
const payload = await api.postgresLogs(state.logsSearch);
state.postgresLogs = payload.logs;
setNotice("Логи отфильтрованы");
+ return;
+ }
+
+ if (action === "set-column-filter") {
+ const column = event.currentTarget.dataset.column;
+ const value = event.currentTarget.value.trim();
+ if (value) {
+ state.columnFilters[column] = value;
+ } else {
+ delete state.columnFilters[column];
+ }
+ return;
+ }
+
+ if (action === "open-create-table-modal") {
+ state.isCreateTableModalOpen = true;
+ return;
+ }
+
+ if (action === "close-create-table-modal") {
+ state.isCreateTableModalOpen = false;
+ return;
+ }
+
+ if (action === "add-table-column") {
+ state.newTableColumns.push({
+ id: Date.now(),
+ name: "",
+ type: "varchar(255)",
+ nullable: true,
+ primaryKey: false
+ });
+ return;
+ }
+
+ if (action === "remove-table-column") {
+ const columnId = Number(event.currentTarget.dataset.columnId);
+ state.newTableColumns = state.newTableColumns.filter((column) => column.id !== columnId);
+ if (!state.newTableColumns.length) {
+ state.newTableColumns = [{ id: Date.now(), name: "", type: "varchar(255)", nullable: true, primaryKey: false }];
+ }
+ return;
+ }
+
+ if (action === "delete-table") {
+ if (!state.activeTable) {
+ throw new Error("Сначала выберите таблицу");
+ }
+
+ await api.deleteTable(state.activeTable);
+ state.activeTable = "";
+ state.selectedRow = null;
+ state.tableDetails = { columns: [], foreignKeys: [], indexes: [] };
+ state.rows = { rows: [], total: 0 };
+ await hydrateDashboard();
+ setNotice("Таблица удалена");
}
}
@@ -352,6 +422,35 @@ async function handleCreateIndex(event) {
setNotice("Индекс создан");
}
+async function handleCreateTable(event) {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+ const tableName = String(formData.get("tableName") || "").trim();
+
+ if (!tableName) {
+ throw new Error("Введите название таблицы");
+ }
+
+ const columns = state.newTableColumns.map((column) => ({
+ name: String(formData.get(`column-name-${column.id}`) || "").trim(),
+ type: String(formData.get(`column-type-${column.id}`) || "").trim(),
+ nullable: formData.get(`column-nullable-${column.id}`) === "on",
+ primaryKey: formData.get(`column-primary-${column.id}`) === "on"
+ }));
+
+ if (columns.some((column) => !column.name || !column.type)) {
+ throw new Error("У каждой колонки должны быть имя и тип");
+ }
+
+ await api.createTable({ tableName, columns });
+ state.isCreateTableModalOpen = false;
+ state.newTableColumns = [{ id: Date.now(), name: "", type: "varchar(255)", nullable: true, primaryKey: false }];
+ await hydrateDashboard();
+ state.activeTable = tableName;
+ await refreshTableState();
+ setNotice("Таблица создана");
+}
+
function formToPayload(form) {
const formData = new FormData(form);
const payload = {};
diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css
index c626551..a5c17a5 100644
--- a/frontend/src/styles/main.css
+++ b/frontend/src/styles/main.css
@@ -451,6 +451,16 @@ textarea {
margin-bottom: 20px;
}
+.filter-panel {
+ margin-bottom: 20px;
+}
+
+.filter-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 14px;
+}
+
.subpanel {
padding: 20px;
border-radius: var(--radius-lg);
@@ -473,6 +483,10 @@ textarea {
gap: 10px;
}
+.inline-check {
+ white-space: nowrap;
+}
+
.checkbox-field input {
width: auto;
}
@@ -567,6 +581,48 @@ td {
word-break: break-word;
}
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 80;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+ background: rgba(2, 9, 19, 0.72);
+ backdrop-filter: blur(8px);
+}
+
+.modal-card {
+ width: min(980px, 100%);
+ max-height: 88vh;
+ overflow: auto;
+ padding: 24px;
+ border-radius: 28px;
+ border: 1px solid rgba(137, 181, 255, 0.16);
+ background: rgba(10, 21, 38, 0.98);
+ box-shadow: var(--shadow);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ align-items: flex-start;
+ margin-bottom: 18px;
+}
+
+.table-column-builder {
+ display: grid;
+ gap: 12px;
+}
+
+.builder-row {
+ display: grid;
+ grid-template-columns: 1.3fr 1.2fr auto auto auto;
+ gap: 10px;
+ align-items: center;
+}
+
.table-toolbar {
justify-content: space-between;
margin: 18px 0;
@@ -614,7 +670,8 @@ button:disabled {
.hero-card,
.section-header,
.subpanel-header,
- .table-toolbar {
+ .table-toolbar,
+ .modal-header {
flex-direction: column;
align-items: stretch;
}
@@ -633,4 +690,8 @@ button:disabled {
.login-form {
padding: 20px;
}
+
+ .builder-row {
+ grid-template-columns: 1fr;
+ }
}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e2a92cf
--- /dev/null
+++ b/index.html
@@ -0,0 +1,1452 @@
+
+
+
+
+
+
PostgreSQL SensoLab Panel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
PostgreSQL SensoLab
+
Войдите для управления базой данных
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Выберите таблицу из списка слева или выполните SQL-запрос
+
+
+
+
+
+
+
+ Страница 1 из 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SQL Query Editor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Создать новую таблицу
+
+
+
+
+
+
+
Если указано, имя таблицы будет создано как папка__имя. Оставьте пустым для создания в корне.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Добавить запись
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Структура таблицы:
+
+
+
+
+
+
+
+
+
+ | Колонка |
+ Тип |
+ NULL |
+ По умолчанию |
+ Действия |
+
+
+
+
+
+
+
+
+
+
+
+
+
Индексы таблицы:
+
+
+
+
+
+
+
+
+
+ | Название |
+ Колонки |
+ Тип |
+ Уникальный |
+ Действия |
+
+
+
+
+
+
+
+
+
+
+
+
+
Добавить колонку
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Создать индекс
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+