в меню иконки items соответствуют их типу
This commit is contained in:
parent
f3508bf7f9
commit
c1547cc24e
@ -250,8 +250,9 @@ ecosystem/
|
|||||||
| | `tags` | Массив строк для поиска |
|
| | `tags` | Массив строк для поиска |
|
||||||
| | `id` | Стабильный ключ (DOM, фокус из дерева) |
|
| | `id` | Стабильный ключ (DOM, фокус из дерева) |
|
||||||
| | `number` | Отображаемый номер |
|
| | `number` | Отображаемый номер |
|
||||||
|
| | `icon` | Опционально: свой путь к SVG (иначе — иконка из `type`) |
|
||||||
|
|
||||||
- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON
|
- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON; определяет иконку и подпись на карточке
|
||||||
- `id` элемента нужен, если нет `number` — иначе ключ генерируется автоматически
|
- `id` элемента нужен, если нет `number` — иначе ключ генерируется автоматически
|
||||||
|
|
||||||
### Примеры данных
|
### Примеры данных
|
||||||
@ -283,6 +284,8 @@ npx serve .
|
|||||||
|
|
||||||
`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp`
|
`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp`
|
||||||
|
|
||||||
|
Каждый тип имеет собственную SVG в `assets/` (например, `postgresql.svg`, `cloudbeaver.svg`). Иконка показывается на карточке каталога и в дереве навигации.
|
||||||
|
|
||||||
Переопределение или добавление типов в JSON:
|
Переопределение или добавление типов в JSON:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
1
assets/cloudbeaver.svg
Normal file
1
assets/cloudbeaver.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M34 88c0-18 14-32 32-34 4-18 20-30 38-30 22 0 40 16 40 38 0 2 0 4-.2 6 14 4 24 16 24 32 0 18-14 32-32 32H52c-10 0-18-8-18-18v-26z" fill="white" fill-opacity=".95"/><ellipse cx="80" cy="92" rx="34" ry="14" fill="white"/><path d="M46 92v24c0 8 15 14 34 14s34-6 34-14V92" fill="white" fill-opacity=".88"/><path d="M46 104c0 8 15 14 34 14s34-6 34-14" stroke="#1565C0" stroke-width="6"/><circle cx="66" cy="72" r="8" fill="#1565C0"/><circle cx="94" cy="72" r="8" fill="#1565C0"/><path d="M72 86h16" stroke="#1565C0" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#64B5F6"/><stop offset="1" stop-color="#1565C0"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 876 B |
1
assets/postgresql.svg
Normal file
1
assets/postgresql.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 98c0-30 15-52 34-52s34 22 34 52c0 14-9 26-21 30v16H67v-16c-12-4-21-16-21-30z" fill="white"/><circle cx="69" cy="78" r="6" fill="#336791"/><circle cx="91" cy="78" r="6" fill="#336791"/><path d="M46 98c-10 10-16 24-16 38h18c2-14 9-26 18-32" fill="white" fill-opacity=".92"/><path d="M72 92c4 3 8 3 12 0" stroke="#336791" stroke-width="5" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5B9BD5"/><stop offset="1" stop-color="#336791"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 688 B |
1
examples/shared/assets/cloudbeaver.svg
Normal file
1
examples/shared/assets/cloudbeaver.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M34 88c0-18 14-32 32-34 4-18 20-30 38-30 22 0 40 16 40 38 0 2 0 4-.2 6 14 4 24 16 24 32 0 18-14 32-32 32H52c-10 0-18-8-18-18v-26z" fill="white" fill-opacity=".95"/><ellipse cx="80" cy="92" rx="34" ry="14" fill="white"/><path d="M46 92v24c0 8 15 14 34 14s34-6 34-14V92" fill="white" fill-opacity=".88"/><path d="M46 104c0 8 15 14 34 14s34-6 34-14" stroke="#1565C0" stroke-width="6"/><circle cx="66" cy="72" r="8" fill="#1565C0"/><circle cx="94" cy="72" r="8" fill="#1565C0"/><path d="M72 86h16" stroke="#1565C0" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#64B5F6"/><stop offset="1" stop-color="#1565C0"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 876 B |
1
examples/shared/assets/postgresql.svg
Normal file
1
examples/shared/assets/postgresql.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 98c0-30 15-52 34-52s34 22 34 52c0 14-9 26-21 30v16H67v-16c-12-4-21-16-21-30z" fill="white"/><circle cx="69" cy="78" r="6" fill="#336791"/><circle cx="91" cy="78" r="6" fill="#336791"/><path d="M46 98c-10 10-16 24-16 38h18c2-14 9-26 18-32" fill="white" fill-opacity=".92"/><path d="M72 92c4 3 8 3 12 0" stroke="#336791" stroke-width="5" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5B9BD5"/><stop offset="1" stop-color="#336791"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 688 B |
@ -33,7 +33,7 @@ let panelTitle = "Панель экосистемы";
|
|||||||
|
|
||||||
const defaultServiceMeta = {
|
const defaultServiceMeta = {
|
||||||
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
||||||
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
|
cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
|
||||||
config: { label: "Config", icon: "assets/config.svg" },
|
config: { label: "Config", icon: "assets/config.svg" },
|
||||||
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
|
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
|
||||||
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
|
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
|
||||||
@ -42,7 +42,7 @@ const defaultServiceMeta = {
|
|||||||
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
|
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
|
||||||
messages: { label: "Messages", icon: "assets/messages.svg" },
|
messages: { label: "Messages", icon: "assets/messages.svg" },
|
||||||
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
|
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
|
||||||
postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
|
postgresql: { label: "PostgreSQL", icon: "assets/postgresql.svg" },
|
||||||
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
|
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
|
||||||
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
|
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
|
||||||
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
|
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
|
||||||
@ -614,9 +614,23 @@ function normalizeItemFromConfig(item, index = 0) {
|
|||||||
type: String(item.type),
|
type: String(item.type),
|
||||||
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
|
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
|
||||||
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
|
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
|
||||||
|
icon: typeof item.icon === "string" && item.icon.trim() ? item.icon.trim() : "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getItemMeta(item) {
|
||||||
|
const typeMeta = serviceMeta[item.type] || serviceMeta.config;
|
||||||
|
|
||||||
|
if (item.icon) {
|
||||||
|
return {
|
||||||
|
label: typeMeta.label,
|
||||||
|
icon: item.icon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeMeta;
|
||||||
|
}
|
||||||
|
|
||||||
function slugifyItemKey(value) {
|
function slugifyItemKey(value) {
|
||||||
return normalize(String(value))
|
return normalize(String(value))
|
||||||
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
|
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
|
||||||
@ -782,7 +796,7 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
|
|||||||
// Создает конечную карточку навигации.
|
// Создает конечную карточку навигации.
|
||||||
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
|
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
|
||||||
function createCardElement(item, query = "") {
|
function createCardElement(item, query = "") {
|
||||||
const meta = serviceMeta[item.type] || serviceMeta.config;
|
const meta = getItemMeta(item);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.className = "nav-card";
|
link.className = "nav-card";
|
||||||
link.href = item.url;
|
link.href = item.url;
|
||||||
@ -950,11 +964,16 @@ function createTreeItemList(items, query = "", className = "tree-items") {
|
|||||||
itemList.className = className;
|
itemList.className = className;
|
||||||
|
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
|
const meta = getItemMeta(item);
|
||||||
const itemButton = document.createElement("button");
|
const itemButton = document.createElement("button");
|
||||||
itemButton.type = "button";
|
itemButton.type = "button";
|
||||||
itemButton.className = "tree-node tree-file";
|
itemButton.className = "tree-node tree-file";
|
||||||
itemButton.dataset.focusTarget = getItemId(item);
|
itemButton.dataset.focusTarget = getItemId(item);
|
||||||
itemButton.innerHTML = `<span>${highlightText(item.title, query)}</span><small>${highlightText(item.tags.join(", "), query)}</small>`;
|
itemButton.innerHTML = `
|
||||||
|
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
|
||||||
|
<span>${highlightText(item.title, query)}</span>
|
||||||
|
<small>${highlightText(item.tags.join(", "), query)}</small>
|
||||||
|
`;
|
||||||
itemList.append(itemButton);
|
itemList.append(itemButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -325,11 +325,17 @@ html[data-tree-two-column="true"] .tree-items {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-tree-two-column="true"] .tree-node.tree-file {
|
html[data-tree-two-column="true"] .tree-node.tree-file {
|
||||||
grid-template-columns: 16px minmax(0, 1fr);
|
grid-template-columns: 20px minmax(0, 1fr);
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-tree-two-column="true"] .tree-node-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-tree-two-column="true"] .tree-node.tree-file small {
|
html[data-tree-two-column="true"] .tree-node.tree-file small {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -930,12 +936,19 @@ html[data-theme="light"] .accent-swatch-custom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree-file {
|
.tree-file {
|
||||||
grid-template-columns: 18px minmax(0, 1fr) auto;
|
grid-template-columns: 20px minmax(0, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-file::before {
|
.tree-file::before {
|
||||||
content: "📄";
|
content: none;
|
||||||
font-size: 0.86rem;
|
}
|
||||||
|
|
||||||
|
.tree-node-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 5px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Контейнер элементов внутри группы дерева. */
|
/* Контейнер элементов внутри группы дерева. */
|
||||||
|
|||||||
27
script.js
27
script.js
@ -33,7 +33,7 @@ let panelTitle = "Панель экосистемы";
|
|||||||
|
|
||||||
const defaultServiceMeta = {
|
const defaultServiceMeta = {
|
||||||
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
||||||
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
|
cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
|
||||||
config: { label: "Config", icon: "assets/config.svg" },
|
config: { label: "Config", icon: "assets/config.svg" },
|
||||||
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
|
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
|
||||||
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
|
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
|
||||||
@ -42,7 +42,7 @@ const defaultServiceMeta = {
|
|||||||
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
|
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
|
||||||
messages: { label: "Messages", icon: "assets/messages.svg" },
|
messages: { label: "Messages", icon: "assets/messages.svg" },
|
||||||
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
|
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
|
||||||
postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
|
postgresql: { label: "PostgreSQL", icon: "assets/postgresql.svg" },
|
||||||
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
|
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
|
||||||
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
|
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
|
||||||
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
|
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
|
||||||
@ -614,9 +614,23 @@ function normalizeItemFromConfig(item, index = 0) {
|
|||||||
type: String(item.type),
|
type: String(item.type),
|
||||||
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
|
tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
|
||||||
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
|
url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#",
|
||||||
|
icon: typeof item.icon === "string" && item.icon.trim() ? item.icon.trim() : "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getItemMeta(item) {
|
||||||
|
const typeMeta = serviceMeta[item.type] || serviceMeta.config;
|
||||||
|
|
||||||
|
if (item.icon) {
|
||||||
|
return {
|
||||||
|
label: typeMeta.label,
|
||||||
|
icon: item.icon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeMeta;
|
||||||
|
}
|
||||||
|
|
||||||
function slugifyItemKey(value) {
|
function slugifyItemKey(value) {
|
||||||
return normalize(String(value))
|
return normalize(String(value))
|
||||||
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
|
.replace(/[^a-z0-9\u0430-\u044f\u0451]+/g, "-")
|
||||||
@ -782,7 +796,7 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
|
|||||||
// Создает конечную карточку навигации.
|
// Создает конечную карточку навигации.
|
||||||
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
|
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
|
||||||
function createCardElement(item, query = "") {
|
function createCardElement(item, query = "") {
|
||||||
const meta = serviceMeta[item.type] || serviceMeta.config;
|
const meta = getItemMeta(item);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.className = "nav-card";
|
link.className = "nav-card";
|
||||||
link.href = item.url;
|
link.href = item.url;
|
||||||
@ -950,11 +964,16 @@ function createTreeItemList(items, query = "", className = "tree-items") {
|
|||||||
itemList.className = className;
|
itemList.className = className;
|
||||||
|
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
|
const meta = getItemMeta(item);
|
||||||
const itemButton = document.createElement("button");
|
const itemButton = document.createElement("button");
|
||||||
itemButton.type = "button";
|
itemButton.type = "button";
|
||||||
itemButton.className = "tree-node tree-file";
|
itemButton.className = "tree-node tree-file";
|
||||||
itemButton.dataset.focusTarget = getItemId(item);
|
itemButton.dataset.focusTarget = getItemId(item);
|
||||||
itemButton.innerHTML = `<span>${highlightText(item.title, query)}</span><small>${highlightText(item.tags.join(", "), query)}</small>`;
|
itemButton.innerHTML = `
|
||||||
|
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
|
||||||
|
<span>${highlightText(item.title, query)}</span>
|
||||||
|
<small>${highlightText(item.tags.join(", "), query)}</small>
|
||||||
|
`;
|
||||||
itemList.append(itemButton);
|
itemList.append(itemButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
21
styles.css
21
styles.css
@ -325,11 +325,17 @@ html[data-tree-two-column="true"] .tree-items {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-tree-two-column="true"] .tree-node.tree-file {
|
html[data-tree-two-column="true"] .tree-node.tree-file {
|
||||||
grid-template-columns: 16px minmax(0, 1fr);
|
grid-template-columns: 20px minmax(0, 1fr);
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-tree-two-column="true"] .tree-node-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-tree-two-column="true"] .tree-node.tree-file small {
|
html[data-tree-two-column="true"] .tree-node.tree-file small {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -930,12 +936,19 @@ html[data-theme="light"] .accent-swatch-custom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree-file {
|
.tree-file {
|
||||||
grid-template-columns: 18px minmax(0, 1fr) auto;
|
grid-template-columns: 20px minmax(0, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-file::before {
|
.tree-file::before {
|
||||||
content: "📄";
|
content: none;
|
||||||
font-size: 0.86rem;
|
}
|
||||||
|
|
||||||
|
.tree-node-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 5px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Контейнер элементов внутри группы дерева. */
|
/* Контейнер элементов внутри группы дерева. */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user