feat: reorder card layout + svc_cred_hint field for credentials note
This commit is contained in:
+4
-1
@@ -199,6 +199,7 @@ class Service(Base):
|
|||||||
comment: Mapped[str] = mapped_column(Text, default="")
|
comment: Mapped[str] = mapped_column(Text, default="")
|
||||||
svc_login: Mapped[str] = mapped_column(String(256), default="")
|
svc_login: Mapped[str] = mapped_column(String(256), default="")
|
||||||
svc_password: Mapped[str] = mapped_column(String(256), default="")
|
svc_password: Mapped[str] = mapped_column(String(256), default="")
|
||||||
|
svc_cred_hint: Mapped[str] = mapped_column(Text, default="")
|
||||||
icon_path: Mapped[str] = mapped_column(Text, default="")
|
icon_path: Mapped[str] = mapped_column(Text, default="")
|
||||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
@@ -1259,6 +1260,7 @@ def ensure_schema_compatibility() -> None:
|
|||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS comment TEXT NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS comment TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_login VARCHAR(256) NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_login VARCHAR(256) NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_password VARCHAR(256) NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_password VARCHAR(256) NOT NULL DEFAULT ''"))
|
||||||
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_cred_hint TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
text(
|
text(
|
||||||
@@ -2683,6 +2685,7 @@ def create_service(payload: dict, request: Request, _: User = Depends(require_ad
|
|||||||
comment=payload.get("comment", ""),
|
comment=payload.get("comment", ""),
|
||||||
svc_login=payload.get("svc_login", ""),
|
svc_login=payload.get("svc_login", ""),
|
||||||
svc_password=payload.get("svc_password", ""),
|
svc_password=payload.get("svc_password", ""),
|
||||||
|
svc_cred_hint=payload.get("svc_cred_hint", ""),
|
||||||
active=payload.get("active", True),
|
active=payload.get("active", True),
|
||||||
warm_pool_size=max(0, int(payload.get("warm_pool_size", 0))),
|
warm_pool_size=max(0, int(payload.get("warm_pool_size", 0))),
|
||||||
)
|
)
|
||||||
@@ -2747,7 +2750,7 @@ def edit_service(service_id: int, payload: dict, request: Request, _: User = Dep
|
|||||||
service = db.get(Service, service_id)
|
service = db.get(Service, service_id)
|
||||||
if not service:
|
if not service:
|
||||||
raise HTTPException(status_code=404, detail="Service not found")
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
for key in ["name", "slug", "target", "active", "comment", "svc_login", "svc_password"]:
|
for key in ["name", "slug", "target", "active", "comment", "svc_login", "svc_password", "svc_cred_hint"]:
|
||||||
if key in payload:
|
if key in payload:
|
||||||
setattr(service, key, payload[key])
|
setattr(service, key, payload[key])
|
||||||
if "type" in payload:
|
if "type" in payload:
|
||||||
|
|||||||
@@ -890,3 +890,10 @@ button {
|
|||||||
|
|
||||||
|
|
||||||
.svc-credentials + .tile-comment { margin-top: 0.5rem; }
|
.svc-credentials + .tile-comment { margin-top: 0.5rem; }
|
||||||
|
|
||||||
|
.svc-cred-hint {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #4a7090;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
<input class="list-search" id="web_search" placeholder="Поиск WEB сервиса..." oninput="filterList('web_search', '#web_list .web-item')" />
|
<input class="list-search" id="web_search" placeholder="Поиск WEB сервиса..." oninput="filterList('web_search', '#web_list .web-item')" />
|
||||||
<div class="list-box" id="web_list">
|
<div class="list-box" id="web_list">
|
||||||
{% for s in web_services %}
|
{% for s in web_services %}
|
||||||
<button class="list-item service-row web-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectWebService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}})'>
|
<button class="list-item service-row web-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectWebService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}}, {{s.svc_cred_hint|tojson}})'>
|
||||||
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||||
<div>
|
<div>
|
||||||
<div>{{s.name}}</div>
|
<div>{{s.name}}</div>
|
||||||
@@ -168,6 +168,11 @@
|
|||||||
<input id="w_svc_password" placeholder="Пароль для входа в сервис" />
|
<input id="w_svc_password" placeholder="Пароль для входа в сервис" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="field-col">
|
||||||
|
<span>Подсказка к логину/паролю (необязательно)</span>
|
||||||
|
<input id="w_svc_cred_hint" placeholder="Например: учётная запись гостя, сбрасывается раз в месяц" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="field-col">
|
<label class="field-col">
|
||||||
<span>Статус</span>
|
<span>Статус</span>
|
||||||
<select id="w_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="w_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
@@ -243,6 +248,7 @@
|
|||||||
<textarea id="new_w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
<textarea id="new_w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
||||||
<input id="new_w_svc_login" placeholder="Логин сервиса (необязательно)" />
|
<input id="new_w_svc_login" placeholder="Логин сервиса (необязательно)" />
|
||||||
<input id="new_w_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
<input id="new_w_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
||||||
|
<input id="new_w_svc_cred_hint" placeholder="Подсказка к логину/паролю (необязательно)" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="field-col">
|
<label class="field-col">
|
||||||
@@ -269,7 +275,7 @@
|
|||||||
<input class="list-search" id="rdp_search" placeholder="Поиск RDP сервиса..." oninput="filterList('rdp_search', '#rdp_list .rdp-item')" />
|
<input class="list-search" id="rdp_search" placeholder="Поиск RDP сервиса..." oninput="filterList('rdp_search', '#rdp_list .rdp-item')" />
|
||||||
<div class="list-box" id="rdp_list">
|
<div class="list-box" id="rdp_list">
|
||||||
{% for s in rdp_services %}
|
{% for s in rdp_services %}
|
||||||
<button class="list-item service-row rdp-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectRdpService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.warm_pool_size}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}})'>
|
<button class="list-item service-row rdp-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectRdpService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.warm_pool_size}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}}, {{s.svc_cred_hint|tojson}})'>
|
||||||
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||||
<div>
|
<div>
|
||||||
<div>{{s.name}}</div>
|
<div>{{s.name}}</div>
|
||||||
@@ -302,6 +308,7 @@
|
|||||||
<textarea id="r_comment" placeholder="Описание для пользователя"></textarea>
|
<textarea id="r_comment" placeholder="Описание для пользователя"></textarea>
|
||||||
<input id="r_svc_login" placeholder="Логин сервиса (показывается на карточке)" />
|
<input id="r_svc_login" placeholder="Логин сервиса (показывается на карточке)" />
|
||||||
<input id="r_svc_password" placeholder="Пароль сервиса (показывается на карточке)" />
|
<input id="r_svc_password" placeholder="Пароль сервиса (показывается на карточке)" />
|
||||||
|
<input id="r_svc_cred_hint" placeholder="Подсказка к логину/паролю (необязательно)" />
|
||||||
<input id="r_pool" type="number" min="0" placeholder="Количество заранее прогретых слотов" />
|
<input id="r_pool" type="number" min="0" placeholder="Количество заранее прогретых слотов" />
|
||||||
<select id="r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,6 +394,7 @@
|
|||||||
<textarea id="new_r_comment" placeholder="Описание для пользователя"></textarea>
|
<textarea id="new_r_comment" placeholder="Описание для пользователя"></textarea>
|
||||||
<input id="new_r_svc_login" placeholder="Логин сервиса (необязательно)" />
|
<input id="new_r_svc_login" placeholder="Логин сервиса (необязательно)" />
|
||||||
<input id="new_r_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
<input id="new_r_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
||||||
|
<input id="new_r_svc_cred_hint" placeholder="Подсказка к логину/паролю (необязательно)" />
|
||||||
<input id="new_r_pool" type="number" min="0" value="1" placeholder="Количество прогретых слотов" />
|
<input id="new_r_pool" type="number" min="0" value="1" placeholder="Количество прогретых слотов" />
|
||||||
<select id="new_r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="new_r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
</div>
|
</div>
|
||||||
@@ -725,7 +733,7 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectWebService(id, name, slug, target, comment, iconPath, active, svcLogin, svcPassword) {
|
function selectWebService(id, name, slug, target, comment, iconPath, active, svcLogin, svcPassword, svcCredHint) {
|
||||||
document.getElementById('w_id').value = id;
|
document.getElementById('w_id').value = id;
|
||||||
document.getElementById('w_name').value = name;
|
document.getElementById('w_name').value = name;
|
||||||
document.getElementById('w_slug').value = slug;
|
document.getElementById('w_slug').value = slug;
|
||||||
@@ -733,6 +741,7 @@
|
|||||||
document.getElementById('w_comment').value = comment || '';
|
document.getElementById('w_comment').value = comment || '';
|
||||||
document.getElementById('w_svc_login').value = svcLogin || '';
|
document.getElementById('w_svc_login').value = svcLogin || '';
|
||||||
document.getElementById('w_svc_password').value = svcPassword || '';
|
document.getElementById('w_svc_password').value = svcPassword || '';
|
||||||
|
document.getElementById('w_svc_cred_hint').value = svcCredHint || '';
|
||||||
document.getElementById('w_active').value = String(active);
|
document.getElementById('w_active').value = String(active);
|
||||||
document.getElementById('w_icon_preview').src = iconPath || placeholderIcon;
|
document.getElementById('w_icon_preview').src = iconPath || placeholderIcon;
|
||||||
setCategoryChecks('.w_cat', serviceCategoryMap[id] || []);
|
setCategoryChecks('.w_cat', serviceCategoryMap[id] || []);
|
||||||
@@ -757,6 +766,7 @@
|
|||||||
comment: document.getElementById('new_w_comment').value,
|
comment: document.getElementById('new_w_comment').value,
|
||||||
svc_login: document.getElementById('new_w_svc_login').value,
|
svc_login: document.getElementById('new_w_svc_login').value,
|
||||||
svc_password: document.getElementById('new_w_svc_password').value,
|
svc_password: document.getElementById('new_w_svc_password').value,
|
||||||
|
svc_cred_hint: document.getElementById('new_w_svc_cred_hint').value,
|
||||||
category_ids: checkedCategoryIds('.new_w_cat'),
|
category_ids: checkedCategoryIds('.new_w_cat'),
|
||||||
active: document.getElementById('new_w_active').value === 'true',
|
active: document.getElementById('new_w_active').value === 'true',
|
||||||
});
|
});
|
||||||
@@ -775,6 +785,7 @@
|
|||||||
comment: document.getElementById('w_comment').value,
|
comment: document.getElementById('w_comment').value,
|
||||||
svc_login: document.getElementById('w_svc_login').value,
|
svc_login: document.getElementById('w_svc_login').value,
|
||||||
svc_password: document.getElementById('w_svc_password').value,
|
svc_password: document.getElementById('w_svc_password').value,
|
||||||
|
svc_cred_hint: document.getElementById('w_svc_cred_hint').value,
|
||||||
category_ids: checkedCategoryIds('.w_cat'),
|
category_ids: checkedCategoryIds('.w_cat'),
|
||||||
active: document.getElementById('w_active').value === 'true',
|
active: document.getElementById('w_active').value === 'true',
|
||||||
});
|
});
|
||||||
@@ -782,7 +793,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearWebForm() {
|
function clearWebForm() {
|
||||||
['w_id','w_name','w_slug','w_target','w_comment','w_svc_login','w_svc_password'].forEach(id => document.getElementById(id).value = '');
|
['w_id','w_name','w_slug','w_target','w_comment','w_svc_login','w_svc_password','w_svc_cred_hint'].forEach(id => document.getElementById(id).value = '');
|
||||||
document.getElementById('w_active').value = 'true';
|
document.getElementById('w_active').value = 'true';
|
||||||
setCategoryChecks('.w_cat', []);
|
setCategoryChecks('.w_cat', []);
|
||||||
document.getElementById('w_icon_preview').src = placeholderIcon;
|
document.getElementById('w_icon_preview').src = placeholderIcon;
|
||||||
@@ -865,7 +876,7 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRdpService(id, name, slug, target, comment, iconPath, active, pool, svcLogin, svcPassword) {
|
function selectRdpService(id, name, slug, target, comment, iconPath, active, pool, svcLogin, svcPassword, svcCredHint) {
|
||||||
const cfg = parseRdpTarget(target);
|
const cfg = parseRdpTarget(target);
|
||||||
document.getElementById('r_id').value = id;
|
document.getElementById('r_id').value = id;
|
||||||
document.getElementById('r_name').value = name;
|
document.getElementById('r_name').value = name;
|
||||||
@@ -878,6 +889,7 @@
|
|||||||
document.getElementById('r_comment').value = comment || '';
|
document.getElementById('r_comment').value = comment || '';
|
||||||
document.getElementById('r_svc_login').value = svcLogin || '';
|
document.getElementById('r_svc_login').value = svcLogin || '';
|
||||||
document.getElementById('r_svc_password').value = svcPassword || '';
|
document.getElementById('r_svc_password').value = svcPassword || '';
|
||||||
|
document.getElementById('r_svc_cred_hint').value = svcCredHint || '';
|
||||||
document.getElementById('r_active').value = String(active);
|
document.getElementById('r_active').value = String(active);
|
||||||
document.getElementById('r_pool').value = pool;
|
document.getElementById('r_pool').value = pool;
|
||||||
setCategoryChecks('.r_cat', serviceCategoryMap[id] || []);
|
setCategoryChecks('.r_cat', serviceCategoryMap[id] || []);
|
||||||
@@ -899,6 +911,7 @@
|
|||||||
comment: document.getElementById('new_r_comment').value,
|
comment: document.getElementById('new_r_comment').value,
|
||||||
svc_login: document.getElementById('new_r_svc_login').value,
|
svc_login: document.getElementById('new_r_svc_login').value,
|
||||||
svc_password: document.getElementById('new_r_svc_password').value,
|
svc_password: document.getElementById('new_r_svc_password').value,
|
||||||
|
svc_cred_hint: document.getElementById('new_r_svc_cred_hint').value,
|
||||||
category_ids: checkedCategoryIds('.new_r_cat'),
|
category_ids: checkedCategoryIds('.new_r_cat'),
|
||||||
warm_pool_size: parseInt(document.getElementById('new_r_pool').value || '0', 10),
|
warm_pool_size: parseInt(document.getElementById('new_r_pool').value || '0', 10),
|
||||||
active: document.getElementById('new_r_active').value === 'true',
|
active: document.getElementById('new_r_active').value === 'true',
|
||||||
@@ -918,6 +931,7 @@
|
|||||||
comment: document.getElementById('r_comment').value,
|
comment: document.getElementById('r_comment').value,
|
||||||
svc_login: document.getElementById('r_svc_login').value,
|
svc_login: document.getElementById('r_svc_login').value,
|
||||||
svc_password: document.getElementById('r_svc_password').value,
|
svc_password: document.getElementById('r_svc_password').value,
|
||||||
|
svc_cred_hint: document.getElementById('r_svc_cred_hint').value,
|
||||||
category_ids: checkedCategoryIds('.r_cat'),
|
category_ids: checkedCategoryIds('.r_cat'),
|
||||||
warm_pool_size: parseInt(document.getElementById('r_pool').value || '0', 10),
|
warm_pool_size: parseInt(document.getElementById('r_pool').value || '0', 10),
|
||||||
active: document.getElementById('r_active').value === 'true',
|
active: document.getElementById('r_active').value === 'true',
|
||||||
@@ -926,7 +940,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearRdpForm() {
|
function clearRdpForm() {
|
||||||
['r_id','r_name','r_slug','r_target','r_host','r_port','r_domain','r_comment','r_pool','r_svc_login','r_svc_password'].forEach(id => document.getElementById(id).value = '');
|
['r_id','r_name','r_slug','r_target','r_host','r_port','r_domain','r_comment','r_pool','r_svc_login','r_svc_password','r_svc_cred_hint'].forEach(id => document.getElementById(id).value = '');
|
||||||
document.getElementById('rdp_slots_box').style.display = 'none';
|
document.getElementById('rdp_slots_box').style.display = 'none';
|
||||||
document.getElementById('r_sec').value = '';
|
document.getElementById('r_sec').value = '';
|
||||||
document.getElementById('r_active').value = 'true';
|
document.getElementById('r_active').value = 'true';
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
<div class="tile-icon-box">
|
<div class="tile-icon-box">
|
||||||
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||||
</div>
|
</div>
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
{% if service.svc_login or service.svc_password %}
|
{% if service.svc_login or service.svc_password %}
|
||||||
<div class="svc-credentials" onclick="event.preventDefault();event.stopPropagation()">
|
<div class="svc-credentials" onclick="event.preventDefault();event.stopPropagation()">
|
||||||
{% if service.svc_login %}
|
{% if service.svc_login %}
|
||||||
@@ -98,13 +99,14 @@
|
|||||||
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_password }}" title="Копировать пароль"></button>
|
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_password }}" title="Копировать пароль"></button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if service.svc_cred_hint %}
|
||||||
|
<p class="svc-cred-hint">{{ service.svc_cred_hint }}</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if service.comment %}
|
{% if service.comment %}
|
||||||
<small class="tile-comment">{{ service_comment_html.get(service.id, '') }}</small>
|
<div class="tile-comment">{{ service_comment_html.get(service.id, '') }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h3>{{ service.name }}</h3>
|
|
||||||
<p>Открыть сервис</p>
|
|
||||||
{% if svc_cats %}
|
{% if svc_cats %}
|
||||||
<div class="service-categories">
|
<div class="service-categories">
|
||||||
{% for category in svc_cats %}
|
{% for category in svc_cats %}
|
||||||
|
|||||||
Reference in New Issue
Block a user