Skip to content

Commit 7bd0064

Browse files
committed
Add admin settings view
1 parent 7c0f465 commit 7bd0064

File tree

8 files changed

+149
-1
lines changed

8 files changed

+149
-1
lines changed

plain-admin/plain/admin/builtin_views.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
"""Built-in admin views for core functionality."""
22

3+
from __future__ import annotations
4+
35
import json
46
from typing import Any
57

6-
from plain.http import RedirectResponse, Response
8+
from plain.http import NotFoundError404, RedirectResponse, Response
9+
from plain.models import QuerySet
10+
from plain.runtime import settings as plain_settings
711

812
from .models import PinnedNavItem
913
from .views.base import AdminView
14+
from .views.objects import AdminListView
1015
from .views.registry import registry
1116

1217
MAX_PINNED_ITEMS = 6
@@ -128,6 +133,82 @@ def post(self) -> Response:
128133
return Response("OK")
129134

130135

136+
def _setting_to_dict(name: str, defn: Any) -> dict[str, Any]:
137+
return {
138+
"name": name,
139+
"source": defn.source,
140+
"value": defn.display_value(),
141+
"env_var_name": defn.env_var_name,
142+
"is_secret": defn.is_secret,
143+
}
144+
145+
146+
class SettingsView(AdminListView):
147+
title = "App Settings"
148+
description = (
149+
"All framework and app settings with their current values and sources."
150+
)
151+
nav_section = None
152+
fields = ["name", "source", "value"]
153+
154+
@classmethod
155+
def get_view_url(cls, obj: Any = None) -> str:
156+
return "/admin/settings/"
157+
158+
search_fields = ["name"]
159+
filters = ["default", "explicit", "env"]
160+
page_size = 100
161+
162+
_FIELD_TEMPLATES = {
163+
"name": ["admin/values/setting_name.html"],
164+
"source": ["admin/values/setting_source.html"],
165+
"value": ["admin/values/setting_value.html"],
166+
}
167+
168+
def get_initial_objects(self) -> list[dict[str, Any]]:
169+
return [
170+
_setting_to_dict(name, defn) for name, defn in plain_settings.get_settings()
171+
]
172+
173+
def filter_objects(
174+
self, objects: list[Any] | QuerySet[Any]
175+
) -> list[Any] | QuerySet[Any]:
176+
if self.filter:
177+
return [obj for obj in objects if obj["source"] == self.filter]
178+
return objects
179+
180+
def format_field_value(self, obj: Any, field: str, value: Any) -> Any:
181+
if field == "source" and obj.get("env_var_name"):
182+
return obj["env_var_name"]
183+
return value
184+
185+
def get_detail_url(self, obj: Any) -> str:
186+
return f"/admin/settings/{obj['name']}/"
187+
188+
def get_field_value_template(self, obj: Any, field: str, value: Any) -> list[str]:
189+
if field in self._FIELD_TEMPLATES:
190+
return self._FIELD_TEMPLATES[field]
191+
return super().get_field_value_template(obj, field, value)
192+
193+
194+
class SettingDetailView(AdminView):
195+
template_name = "admin/setting_detail.html"
196+
parent_view_class = SettingsView
197+
nav_section = None
198+
199+
def get_template_context(self) -> dict[str, Any]:
200+
name = self.url_kwargs["name"]
201+
settings_map = dict(plain_settings.get_settings())
202+
defn = settings_map.get(name)
203+
if defn is None:
204+
raise NotFoundError404()
205+
206+
context = super().get_template_context()
207+
context["title"] = name
208+
context["setting"] = _setting_to_dict(name, defn)
209+
return context
210+
211+
131212
class StyleGuideView(AdminView):
132213
"""Style guide showing available components and patterns."""
133214

plain-admin/plain/admin/templates/admin/_header.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@
5959
{% endif %}
6060
<template>
6161
<div class="py-1">
62+
<a href="{{ url('admin:settings') }}" class="flex items-center gap-2 w-full text-left px-4 py-2 text-sm text-stone-700 hover:bg-stone-100 rounded">
63+
<admin.Icon name="gear" />
64+
App Settings
65+
</a>
6266
<form method="POST" action="{{ url('admin:logout') }}">
6367
<button type="submit" class="flex items-center gap-2 w-full text-left px-4 py-2 text-sm text-stone-700 hover:bg-stone-100 rounded">
6468
<admin.Icon name="box-arrow-right" />
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{% extends "admin/base.html" %}
2+
3+
{% use_elements %}
4+
5+
{% block content %}
6+
7+
<div class="card">
8+
<section>
9+
<dl>
10+
<div class="flex gap-8 py-1.5">
11+
<dt class="w-40 flex-shrink-0 text-black/50">Name</dt>
12+
<dd class="min-w-0">
13+
<span class="font-mono text-sm">{{ setting.name }}</span>
14+
</dd>
15+
</div>
16+
<div class="flex gap-8 py-1.5">
17+
<dt class="w-40 flex-shrink-0 text-black/50">Source</dt>
18+
<dd class="min-w-0">
19+
<span
20+
data-source="{{ setting.source }}"
21+
class="inline-flex items-center px-1.5 py-0.5 text-xs rounded-md
22+
data-[source=default]:bg-stone-100 data-[source=default]:text-stone-400
23+
data-[source=explicit]:bg-stone-200/70 data-[source=explicit]:text-stone-600
24+
data-[source=env]:bg-amber-50 data-[source=env]:text-amber-800 data-[source=env]:font-mono"
25+
>{{ setting.env_var_name or setting.source }}</span>
26+
</dd>
27+
</div>
28+
<div class="flex gap-8 py-1.5">
29+
<dt class="w-40 flex-shrink-0 text-black/50">Value</dt>
30+
<dd class="min-w-0">
31+
<span
32+
data-secret="{{ setting.is_secret }}"
33+
class="font-mono text-sm break-all whitespace-pre-wrap data-[secret=True]:text-stone-400 data-[secret=True]:italic"
34+
>{{ setting.value }}</span>
35+
</dd>
36+
</div>
37+
</dl>
38+
</section>
39+
</div>
40+
41+
{% endblock %}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<span class="font-mono text-sm">{{ value }}</span>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<span
2+
data-source="{{ object.source }}"
3+
class="inline-flex items-center px-1.5 py-0.5 text-xs rounded-md
4+
data-[source=default]:bg-stone-100 data-[source=default]:text-stone-400
5+
data-[source=explicit]:bg-stone-200/70 data-[source=explicit]:text-stone-600
6+
data-[source=env]:bg-amber-50 data-[source=env]:text-amber-800 data-[source=env]:font-mono"
7+
>{{ value }}</span>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% if value == "''" %}
2+
<span class="text-stone-300">&mdash;</span>
3+
{% else %}
4+
<span
5+
data-secret="{{ object.is_secret }}"
6+
class="font-mono text-sm break-all data-[secret=True]:text-stone-400 data-[secret=True]:italic"
7+
>{{ value }}</span>
8+
{% endif %}

plain-admin/plain/admin/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
AdminSearchView,
77
PinNavView,
88
ReorderPinnedView,
9+
SettingDetailView,
10+
SettingsView,
911
StyleGuideView,
1012
UnpinNavView,
1113
)
@@ -20,6 +22,8 @@ class AdminRouter(Router):
2022
urls = [
2123
path("search/", AdminSearchView, name="search"),
2224
path("style/", StyleGuideView, name="style"),
25+
path("settings/", SettingsView, name="settings"),
26+
path("settings/<name>/", SettingDetailView, name="setting_detail"),
2327
path("logout/", LogoutView, name="logout"),
2428
path("_/pin/", PinNavView, name="pin"),
2529
path("_/unpin/", UnpinNavView, name="unpin"),

plain/plain/runtime/user_settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ def get_settings(
248248
self._setup()
249249
result = []
250250
for name, defn in sorted(self._settings.items()):
251+
if name.startswith("_"):
252+
continue
251253
if source is not None and defn.source != source:
252254
continue
253255
result.append((name, defn))

0 commit comments

Comments
 (0)