Show effective access roles in HTML5 workspace
This commit is contained in:
@@ -70,7 +70,8 @@ def render_html5_access_page(
|
|||||||
<div class="panel-title">Группы доступа</div>
|
<div class="panel-title">Группы доступа</div>
|
||||||
<div class="access-list">{''.join(_group_card(item) for item in groups[:80]) or '<p class="muted padded">Группы не найдены</p>'}</div>
|
<div class="access-list">{''.join(_group_card(item) for item in groups[:80]) or '<p class="muted padded">Группы не найдены</p>'}</div>
|
||||||
<div class="panel-title">Пользователи</div>
|
<div class="panel-title">Пользователи</div>
|
||||||
<div class="access-list">{''.join(_user_card(item) for item in users[:80]) or '<p class="muted padded">Пользователи не найдены</p>'}</div>
|
<div data-html5-access-user-detail>{render_html5_access_user_detail(project_id=project_id, user_payload=None)}</div>
|
||||||
|
<div class="access-list">{''.join(_user_card(project_id, item) for item in users[:80]) or '<p class="muted padded">Пользователи не найдены</p>'}</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -157,6 +158,35 @@ def render_html5_access_profile_apply_result(*, project_id: str, response: objec
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_access_user_detail(*, project_id: str, user_payload: dict | None) -> str:
|
||||||
|
if user_payload is None:
|
||||||
|
return """
|
||||||
|
<section class="access-user-detail">
|
||||||
|
<p class="muted padded">Выберите пользователя, чтобы увидеть группы и эффективные роли.</p>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
user = dict(user_payload.get("user") or {})
|
||||||
|
roles = list(user_payload.get("effective_roles") or [])
|
||||||
|
groups = list(user.get("groups") or [])
|
||||||
|
name = str(user.get("name") or "")
|
||||||
|
full_name = str(user.get("full_name") or "")
|
||||||
|
return f"""
|
||||||
|
<section class="access-user-detail" data-html5-access-user="{escape(name)}">
|
||||||
|
<div class="access-plan-head">
|
||||||
|
<span class="status-pill">пользователь</span>
|
||||||
|
<strong>{escape(name)}</strong>
|
||||||
|
</div>
|
||||||
|
<p class="object-summary">{escape(full_name or "ФИО не загружено")}</p>
|
||||||
|
<div class="report-grid">
|
||||||
|
{_metric("Группы", len(groups))}
|
||||||
|
{_metric("Эффективные роли", len(roles))}
|
||||||
|
</div>
|
||||||
|
{_notice_list("Группы пользователя", groups)}
|
||||||
|
<div class="access-role-grid">{''.join(_role_card(_DictRole(item)) for item in roles) or '<p class="muted padded">Эффективные роли не найдены</p>'}</div>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_html5_access_profile(*, project_id: str, profile: object | None) -> str:
|
def render_html5_access_profile(*, project_id: str, profile: object | None) -> str:
|
||||||
if profile is None:
|
if profile is None:
|
||||||
return """
|
return """
|
||||||
@@ -308,11 +338,21 @@ def _group_card(group: object) -> str:
|
|||||||
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(profile)} · {len(users)} пользователей</small></article>'
|
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(profile)} · {len(users)} пользователей</small></article>'
|
||||||
|
|
||||||
|
|
||||||
def _user_card(user: object) -> str:
|
def _user_card(project_id: str, user: object) -> str:
|
||||||
name = str(getattr(user, "name", ""))
|
name = str(getattr(user, "name", ""))
|
||||||
full_name = str(getattr(user, "full_name", "") or "")
|
full_name = str(getattr(user, "full_name", "") or "")
|
||||||
groups = list(getattr(user, "groups", []) or [])
|
groups = list(getattr(user, "groups", []) or [])
|
||||||
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(full_name)} · {len(groups)} групп</small></article>'
|
return f"""
|
||||||
|
<article
|
||||||
|
class="access-card"
|
||||||
|
hx-get="/html5/projects/{quote(project_id)}/access/users/{quote(name, safe='')}"
|
||||||
|
hx-target="[data-html5-access-user-detail]"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
<strong>{escape(name)}</strong>
|
||||||
|
<small>{escape(full_name)} · {len(groups)} групп</small>
|
||||||
|
</article>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _operation_card(operation: dict) -> str:
|
def _operation_card(operation: dict) -> str:
|
||||||
@@ -359,3 +399,10 @@ class _DictProfile:
|
|||||||
self.roles = payload.get("roles") or []
|
self.roles = payload.get("roles") or []
|
||||||
self.attributes = payload.get("attributes") or {}
|
self.attributes = payload.get("attributes") or {}
|
||||||
self.source = payload.get("source") or "workspace"
|
self.source = payload.get("source") or "workspace"
|
||||||
|
|
||||||
|
|
||||||
|
class _DictRole:
|
||||||
|
def __init__(self, payload: dict):
|
||||||
|
self.role = str(payload.get("role") or payload.get("name") or "")
|
||||||
|
self.role_qualified_name = str(payload.get("role_qualified_name") or payload.get("qualified_name") or self.role)
|
||||||
|
self.source = str(payload.get("source") or "")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from api_server.html5_access import (
|
|||||||
render_html5_access_profile_preview,
|
render_html5_access_profile_preview,
|
||||||
render_html5_access_publish_plan,
|
render_html5_access_publish_plan,
|
||||||
render_html5_access_publish_result,
|
render_html5_access_publish_result,
|
||||||
|
render_html5_access_user_detail,
|
||||||
)
|
)
|
||||||
from api_server.html5_forms import form_value, html5_csv_values
|
from api_server.html5_forms import form_value, html5_csv_values
|
||||||
|
|
||||||
@@ -106,6 +107,15 @@ async def html5_access_profile_apply(
|
|||||||
return render_html5_access_profile_apply_result(project_id=project_id, response=response, plan=plan)
|
return render_html5_access_profile_apply_result(project_id=project_id, response=response, plan=plan)
|
||||||
|
|
||||||
|
|
||||||
|
async def html5_access_user_detail(
|
||||||
|
*,
|
||||||
|
project_id: str,
|
||||||
|
user_name: str,
|
||||||
|
access_user: Callable[[str, str], Awaitable[dict]],
|
||||||
|
) -> str:
|
||||||
|
return render_html5_access_user_detail(project_id=project_id, user_payload=await access_user(project_id, user_name))
|
||||||
|
|
||||||
|
|
||||||
def _selected_profile(
|
def _selected_profile(
|
||||||
normalized: object,
|
normalized: object,
|
||||||
profile_name: str | None,
|
profile_name: str | None,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ from api_server.html5_access_controller import (
|
|||||||
html5_access_profile_preview as _html5_access_profile_preview,
|
html5_access_profile_preview as _html5_access_profile_preview,
|
||||||
html5_access_publish_dry_run as _html5_access_publish_dry_run,
|
html5_access_publish_dry_run as _html5_access_publish_dry_run,
|
||||||
html5_access_publish_plan as _html5_access_publish_plan,
|
html5_access_publish_plan as _html5_access_publish_plan,
|
||||||
|
html5_access_user_detail as _html5_access_user_detail,
|
||||||
)
|
)
|
||||||
from api_server.html5_editor_controller import (
|
from api_server.html5_editor_controller import (
|
||||||
html5_editor_page as _html5_editor_page,
|
html5_editor_page as _html5_editor_page,
|
||||||
@@ -1606,6 +1607,17 @@ async def html5_project_access_profile_publish_dry_run(project_id: str, profile_
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/html5/projects/{project_id}/access/users/{user_name}")
|
||||||
|
async def html5_project_access_user(project_id: str, user_name: str) -> Response:
|
||||||
|
return _html5_response(
|
||||||
|
await _html5_access_user_detail(
|
||||||
|
project_id=project_id,
|
||||||
|
user_name=user_name,
|
||||||
|
access_user=get_project_access_user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/html5/projects/{project_id}/forms/editor/preview")
|
@app.post("/html5/projects/{project_id}/forms/editor/preview")
|
||||||
async def html5_project_form_editor_preview(project_id: str, request: Request) -> Response:
|
async def html5_project_form_editor_preview(project_id: str, request: Request) -> Response:
|
||||||
form = await _html5_form_data(request)
|
form = await _html5_form_data(request)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
.form-empty-structure{grid-column:1/-1;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.form-empty-structure strong,.form-empty-structure span{display:block}.form-empty-structure strong{color:#1f2937}
|
.form-empty-structure{grid-column:1/-1;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.form-empty-structure strong,.form-empty-structure span{display:block}.form-empty-structure strong{color:#1f2937}
|
||||||
.access-workspace{background:#f4f7fb}.access-layout{display:grid;grid-template-columns:300px minmax(0,1fr)340px;height:calc(100vh - 88px)}.access-main{border-top:0;border-bottom:0;overflow:auto}.access-nav,.access-side{overflow:auto}.tree-item[data-html5-access-profile-selected="true"]{background:#f8fbff;border-left:3px solid var(--brand);padding-left:9px}.access-empty{margin:16px;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.access-empty strong,.access-empty span{display:block}.access-empty strong{color:#1f2937}.access-profile,.access-plan,.access-result{border-bottom:1px solid var(--line);background:#fff}.access-summary{display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:#f8fbff;color:#687385;font-size:12px;font-weight:800}.access-role-grid,.access-operations,.access-list{display:grid}.access-role-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.access-card{display:grid;gap:3px;min-width:0;padding:10px 12px;border-bottom:1px solid var(--line)}.access-role-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}.access-card strong,.access-card small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.access-card small{color:var(--muted)}.access-plan-head{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-plan-head form{margin:0}.access-actions{padding:12px}.access-warnings{margin:0;padding:0;list-style:none}.access-warnings li{padding:8px 12px;border-bottom:1px solid var(--line);color:var(--warn);font-weight:800}.access-json{margin:0;max-height:220px;overflow:auto;padding:12px;background:#fbfaf7;border-top:1px solid var(--line);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}.access-main .primary{background:var(--brand);border-color:var(--brand);color:#fff}
|
.access-workspace{background:#f4f7fb}.access-layout{display:grid;grid-template-columns:300px minmax(0,1fr)340px;height:calc(100vh - 88px)}.access-main{border-top:0;border-bottom:0;overflow:auto}.access-nav,.access-side{overflow:auto}.tree-item[data-html5-access-profile-selected="true"]{background:#f8fbff;border-left:3px solid var(--brand);padding-left:9px}.access-empty{margin:16px;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.access-empty strong,.access-empty span{display:block}.access-empty strong{color:#1f2937}.access-profile,.access-plan,.access-result{border-bottom:1px solid var(--line);background:#fff}.access-summary{display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:#f8fbff;color:#687385;font-size:12px;font-weight:800}.access-role-grid,.access-operations,.access-list{display:grid}.access-role-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.access-card{display:grid;gap:3px;min-width:0;padding:10px 12px;border-bottom:1px solid var(--line)}.access-role-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}.access-card strong,.access-card small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.access-card small{color:var(--muted)}.access-plan-head{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-plan-head form{margin:0}.access-actions{padding:12px}.access-warnings{margin:0;padding:0;list-style:none}.access-warnings li{padding:8px 12px;border-bottom:1px solid var(--line);color:var(--warn);font-weight:800}.access-json{margin:0;max-height:220px;overflow:auto;padding:12px;background:#fbfaf7;border-top:1px solid var(--line);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}.access-main .primary{background:var(--brand);border-color:var(--brand);color:#fff}
|
||||||
.access-builder{border-bottom:1px solid var(--line);background:#fff}.access-builder-form{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-builder-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.access-builder-form input,.access-builder-form textarea{min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.access-builder-form input{height:32px}.access-builder-form textarea{min-height:68px;padding:8px;resize:vertical}.access-builder-actions{grid-column:1/-1;display:flex;gap:8px;justify-content:flex-end}.access-builder-result{border-top:1px solid var(--line);background:#fff}
|
.access-builder{border-bottom:1px solid var(--line);background:#fff}.access-builder-form{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-builder-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.access-builder-form input,.access-builder-form textarea{min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.access-builder-form input{height:32px}.access-builder-form textarea{min-height:68px;padding:8px;resize:vertical}.access-builder-actions{grid-column:1/-1;display:flex;gap:8px;justify-content:flex-end}.access-builder-result{border-top:1px solid var(--line);background:#fff}
|
||||||
|
.access-card[hx-get]{cursor:pointer}.access-card[hx-get]:hover{background:#f8fbff}.access-user-detail{border-bottom:1px solid var(--line);background:#fff}
|
||||||
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
|
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
|
||||||
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
|
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
|
||||||
@media(max-width:980px){.access-builder-form{grid-template-columns:1fr}.access-builder-actions{justify-content:stretch}.access-builder-actions button{flex:1}}
|
@media(max-width:980px){.access-builder-form{grid-template-columns:1fr}.access-builder-actions{justify-content:stretch}.access-builder-actions button{flex:1}}
|
||||||
|
|||||||
@@ -1643,6 +1643,9 @@ def test_import_supports_structure_only_indexing(monkeypatch, tmp_path: Path):
|
|||||||
)
|
)
|
||||||
assert_html5_response_contract(html5_apply, "сохранено", "HTML5ПрофильHTTP", "CREATE_ACCESS_PROFILE")
|
assert_html5_response_contract(html5_apply, "сохранено", "HTML5ПрофильHTTP", "CREATE_ACCESS_PROFILE")
|
||||||
|
|
||||||
|
html5_user = client.get(f"/html5/projects/{project_id}/access/users/ivanov")
|
||||||
|
assert_html5_response_contract(html5_user, "пользователь", "ivanov", "Эффективные роли", "Роль.Менеджер")
|
||||||
|
|
||||||
access_plan = client.get(
|
access_plan = client.get(
|
||||||
f"/html5/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/plan"
|
f"/html5/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/plan"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user