Repository: rclement/datasette-dashboards (language: python)

Issue:
Allow dashboards to enter fullscreen
It would be nice for the dashboards to be allowed to toggle a fullscreen mode at the click of a button.

Here is a snippet to toggle fullscreen mode:

```html
<button
  onclick="document.fullscreenElement
    ? document.exitFullscreen()
    : document.body.requestFullscreen()
  "
>
  Toggle Fullscreen
</button>
```

Instead of `document.body` it is more advisable to select a sub-section of the page to be made fullscreen: for instance, it could be only the central dashboarding part (title, description, filters, chart grid) without header nor footer.


Discussion / hints:
Also the background color is lost when entering fullscreen and must be specified using the following selectors:
```css
:fullscreen, ::backdrop {
  background-color: #F8FAFB;
}
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49ea2e1..fb398be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 - Table chart component
 - Map chart component
+- Fullscreen mode for dashboards
 
 ### Changed
 - Improve filters layout
diff --git a/README.md b/README.md
index d2172be..7c13737 100644
--- a/README.md
+++ b/README.md
@@ -92,9 +92,16 @@ Dashboard properties:
 | ------------- | -------- | --------------------- |
 | `title`       | `string` | Dashboard title       |
 | `description` | `string` | Dashboard description |
+| `settings`    | `object` | Dashboard settings    |
 | `layout`      | `array`  | Dashboard layout      |
 | `filters`     | `object` | Dashboard filters     |
 
+Dashboard settings:
+
+| Property           | Type   | Description                                                    |
+| ------------------ | ------ | -------------------------------------------------------------- |
+| `allow_fullscreen` | `bool` | Allow dashboard to be toggled in fullscreen  (default `false`) |
+
 Dashboard filters:
 
 | Property  | Type               | Description                                      |
diff --git a/datasette_dashboards/__init__.py b/datasette_dashboards/__init__.py
index 67196b5..be78563 100644
--- a/datasette_dashboards/__init__.py
+++ b/datasette_dashboards/__init__.py
@@ -96,6 +96,7 @@ async def dashboard_view(request, datasette):
     except KeyError:
         raise NotFound(f"Dashboard not found: {slug}")
 
+    settings = dashboard.get("settings", {})
     dbs = set([chart["db"] for chart in dashboard["charts"].values() if "db" in chart])
     for db in dbs:
         try:
@@ -126,6 +127,7 @@ async def dashboard_view(request, datasette):
         await datasette.render_template(
             "dashboard_view.html",
             {
+                "settings": settings,
                 "slug": slug,
                 "query_parameters": query_parameters,
                 "query_string": query_string,
diff --git a/datasette_dashboards/static/dashboards.css b/datasette_dashboards/static/dashboards.css
index 4196f3c..ba8c3a9 100644
--- a/datasette_dashboards/static/dashboards.css
+++ b/datasette_dashboards/static/dashboards.css
@@ -1,3 +1,22 @@
+:fullscreen, ::backdrop {
+  background-color: #F8FAFB;
+  padding: 0 1rem;
+  overflow: scroll !important;
+}
+
+.dashboard-header {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 1rem;
+}
+
+.dashboard-toolbar {
+  margin-bottom: 1rem;
+}
+
 .dashboard-filters {
   margin-bottom: 1rem;
 }
diff --git a/datasette_dashboards/static/dashboards.js b/datasette_dashboards/static/dashboards.js
index fbe426c..f1a8fed 100644
--- a/datasette_dashboards/static/dashboards.js
+++ b/datasette_dashboards/static/dashboards.js
@@ -192,3 +192,12 @@ async function renderChart(el, chart, query_string, full_height = false) {
     await render(el, chart, query_string, full_height)
   }
 }
+
+function toggleFullscreen() {
+  const el = document.querySelector("section.content")
+  if (document.fullscreenElement) {
+    document.exitFullscreen()
+  } else {
+    el.requestFullscreen()
+  }
+}
diff --git a/datasette_dashboards/templates/dashboard_view.html b/datasette_dashboards/templates/dashboard_view.html
index 385ece3..cb517d9 100644
--- a/datasette_dashboards/templates/dashboard_view.html
+++ b/datasette_dashboards/templates/dashboard_view.html
@@ -38,10 +38,20 @@
 
 {% block content %}
     <div class="dashboard-header">
-      <div class="page-header" style="border-color: black">
-        <h1>{{ dashboard.title }}</h1>
+      <div>
+        <div class="page-header" style="border-color: black">
+          <h1>{{ dashboard.title }}</h1>
+        </div>
+        <p>{{ dashboard.description }}</p>
+      </div>
+
+      <div class="dashboard-toolbar">
+        {% if settings.allow_fullscreen %}
+        <form>
+          <button type="button" onclick="toggleFullscreen()">Fullscreen</button>
+        </form>
+        {% endif %}
       </div>
-      <p>{{ dashboard.description }}</p>
     </div>
 
     {% if dashboard.filters %}
diff --git a/demo/metadata.yml b/demo/metadata.yml
index 730a68e..d757024 100644
--- a/demo/metadata.yml
+++ b/demo/metadata.yml
@@ -9,6 +9,8 @@ plugins:
     job-offers-stats:
       title: Job offers statistics
       description: Gather metrics about job offers
+      settings:
+        allow_fullscreen: true
       layout:
         - [analysis-note, offers-day, offers-day, offers-count]
         - [analysis-note, offers-source, offers-day-source, offers-region]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 4df29c6..4ac8f7a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -35,6 +35,7 @@ def datasette_metadata():
                 "job-dashboard": {
                     "title": "Job dashboard",
                     "description": "Gathering metrics about jobs",
+                    "settings": {"allow_fullscreen": False},
                     "filters": {
                         "date_start": {
                             "name": "Date Start",
diff --git a/tests/test_dashboard_view.py b/tests/test_dashboard_view.py
index c80951e..f9cf839 100644
--- a/tests/test_dashboard_view.py
+++ b/tests/test_dashboard_view.py
@@ -18,6 +18,12 @@ async def test_dashboard_views(datasette):
         assert "grid-template-areas" not in response.text
         assert "grid-area:" not in response.text
 
+        assert '<div class="dashboard-toolbar">' in response.text
+        assert (
+            '<button type="button" onclick="toggleFullscreen()">Fullscreen</button>'
+            not in response.text
+        )
+
         assert '<div class="dashboard-filters">' in response.text
         for key, flt in dashboard["filters"].items():
             expected_type = (
@@ -155,6 +161,25 @@ async def test_dashboard_view_unknown_chart_db(datasette_db, datasette_metadata)
     assert response.status_code == 404
 
 
+@pytest.mark.asyncio
+async def test_dashboard_view_allow_fullscreen(datasette_db, datasette_metadata):
+    metadata = copy.deepcopy(datasette_metadata)
+    metadata["plugins"]["datasette-dashboards"]["job-dashboard"]["settings"][
+        "allow_fullscreen"
+    ] = True
+    datasette = Datasette([str(datasette_db)], metadata=metadata)
+
+    response = await datasette.client.get(
+        "/-/dashboards/job-dashboard", follow_redirects=True
+    )
+    assert response.status_code == 200
+
+    assert (
+        '<button type="button" onclick="toggleFullscreen()">Fullscreen</button>'
+        in response.text
+    )
+
+
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
     "metadata,authenticated,expected_status",