Feature/global redash subdashboards - #79
Conversation
- Add GET /global-api/sub-dashboards, login-gated returning published, non-archived sub-dashboards newest-first with simple page/page_size pagination (no search or sort params yet — listing is all the UI needs for now). - Source the template org slug from TEMPLATE_ORG_SLUG (default se_template) so the deployment controls it rather than hardcoding a convention. - Return an empty page instead of erroring when the template org doesn't exist.
- Add GET /global-api/organizations, login-gated like the rest of the console,
returning every org as {id, name, slug} ordered by name.
- Return a bare list rather than the paginated since it will be used with the picker.
GET/POST/DELETE endpoints for sub-dashboard assignments. Kept thin for now, the endpoints trust the frontend, which only submits values the same API served.
CSRFProtect is already active on the global app, so it enforces the X-CSRFToken header on the write endpoints (assignment create/delete) — but nothing handed the SPA a token to send back, so those writes would have been rejected the moment the frontend called them. This closes that gap without weakening the protection: a cross-site attacker still can't read the cookie, so it still can't forge the token. - Inject a csrf_token cookie on every response in security.init_app, mirroring redash/security.py, so the delivery mechanism lives beside the CSRFProtect config it complements rather than in the SPA view. - Refresh it on every response (not once at page load) because generate_csrf re-stamps the token's timestamp, keeping it from lapsing past CSRF_TIME_LIMIT (6h) while the SPA stays open. - Keep the cookie non-HttpOnly by design so the frontend's JS can read it and echo it in X-CSRFToken; add a test asserting both that it is set and that it stays JS-readable.
…izations. - Add a dedicated global axios instance pointed at /global-api. - Unwrap responses to the body in an interceptor, mirroring the main app so services resolve to data directly instead of the full axios response. - Add the organization and sub-dashboard services covering the list, assignment read, assign, and remove-assignment endpoints.
- Add SubDashboardList.jsx on top of the shared items-list machinery (ResourceItemsSource + UrlStateStorage) so pagination, sorting, and URL-state come for free; default to newest-first. - Link each row to its per-dashboard assignments route. - Register the /sub-dashboards route in GlobalApplicationArea. - Add a Sub-Dashboards item to GlobalDesktopNavbar with a DesktopOutlined icon for discoverability. - Widen the global navbar to 96px and let the label wrap instead of being truncated by overflow:hidden, since "Sub-Dashboards" is too long for the default column on one line.
Client-side routes like /sub-dashboards have no server-side rule, so a hard navigation or refresh returned a 404 instead of loading the app. Adding a catch-all lets the React router resolve these paths.
- Add SubDashboardAssignments page - Track removal per-row via removingId (not a bool) so each Remove button shows its own spinner while the rest are disabled, preventing concurrent mutations without freezing the whole table. - Disable closing the modal while an assign is saving, so the user can't dismiss it mid-request and have it try to update a modal that's gone. - Register the /sub-dashboards/:dashboardId/assignments route so the page is reachable from the sub-dashboard list.
Can happen in case we try to assign at same time even if this is very rare to happen.
|
@imenattatra thank you for the PR - how do I log in https://global-dashboard.staging.metr.systems/sub-dashboards?order=-created_at&page=1&page_size=20 |
thiagogds
left a comment
There was a problem hiding this comment.
Besides my comments, there's a big thing I missed in the last PR, the tests.
You added new tests now in the global_redash and you have some others in the tests directory outside. I think it's better to keep all of them together in global_redash.
Also, you're using unittest and we don't use that anymore anywhere, because global_redash is a metr product, you should keep to the conventions of our team. Other developers will also work on this later and it's easier if we follow the same practices here.
So please use pytest and move the tests to the same place. Also, when using pytest, please use fixtures and don't use base classes similar to unittest.
| def _serialize_dashboard(dashboard): | ||
| metr_dashboard = dashboard.metr_dashboard | ||
| return { | ||
| "id": dashboard.id, | ||
| "name": dashboard.name, | ||
| "slug": dashboard.slug, | ||
| "url_identifier": metr_dashboard.url_identifier if metr_dashboard else None, | ||
| } |
There was a problem hiding this comment.
This is basically the same serializer as the one in subdashboards. Move them to a serializer.py
There was a problem hiding this comment.
I think that we are enforcing Django patterns in a flask application by doing this. I'm personally fine with the serializers in the same file considering the design decision from @imenattatra (that I also agree with) to have one file per endpoint. Considering this pattern, no Django's one, it makes sense to me to have these serialization methods here as it's unlikely that they are gonna be used elsewhere.
But, if at some point we import any one of these methods to re-use them at other points of the code, I'm 100% with moving them to a serializers.py module.
There was a problem hiding this comment.
A solution used by Redash itself is to move them to the models. For me the most important part is that they are exactly the same thing, a Dashboard serializer, so should have the same reason to change
There was a problem hiding this comment.
Thank you ! I had other plans for the serializers actually. We already talked during the POC about using pydantic and i believe that, after we do that, the way we are using serializers in global redash will change.
I am also not sure about adopting the same thing as redash putting them in models because they already regret doing so, they say it here
redash/redash/serializers/__init__.py
Line 2 in e02c02a
I would like to work on this in a separate PR that resolved this newly created issue about adopting pydantic https://github.com/metr-systems/backlog/issues/4981 . What do you think? @berinhard @thiagogds
There was a problem hiding this comment.
Just don't add this task to this milestone. It's a improvement and we need to have the whole global redash done soon
There was a problem hiding this comment.
I moved it to a separate project about "Improvements for Global Redash" since i am only managing the project and not the milestone "Redash Scalability" , I added a comment about that so that @helenalebreton knows
| db.session.commit() | ||
| except IntegrityError: | ||
| db.session.rollback() | ||
| return jsonify({"message": "This dashboard is already assigned to that organization."}), 409 |
There was a problem hiding this comment.
This should be a serializer validation
There was a problem hiding this comment.
As we don't have serializers classes (at least not that I'm aware of), I'm also fine with leaving it as it's. Although I agree that this is a serializer validation, in Django realms, we don't have easy or no-brainer ways of doing that within Flask without installing third party apps like, for example, flask-serialize. I'm not advising us to do that hehehe
There was a problem hiding this comment.
I would add it to pydantic as i mentioned it in the previous command, please check issue https://github.com/metr-systems/backlog/issues/4981
@helenalebreton Sorry forgot that, I added the access created to lastpass under the name of |
@thiagogds Thank you for your review , the tests are not under |
berinhard
left a comment
There was a problem hiding this comment.
The only blocker I have is the same pointed by @thiagogds about the template organization. I really liked the organization of the views as it was easy to compare with the tests with the 1 to 1 file mapping.
| def _serialize_dashboard(dashboard): | ||
| metr_dashboard = dashboard.metr_dashboard | ||
| return { | ||
| "id": dashboard.id, | ||
| "name": dashboard.name, | ||
| "slug": dashboard.slug, | ||
| "url_identifier": metr_dashboard.url_identifier if metr_dashboard else None, | ||
| } |
There was a problem hiding this comment.
I think that we are enforcing Django patterns in a flask application by doing this. I'm personally fine with the serializers in the same file considering the design decision from @imenattatra (that I also agree with) to have one file per endpoint. Considering this pattern, no Django's one, it makes sense to me to have these serialization methods here as it's unlikely that they are gonna be used elsewhere.
But, if at some point we import any one of these methods to re-use them at other points of the code, I'm 100% with moving them to a serializers.py module.
| db.session.commit() | ||
| except IntegrityError: | ||
| db.session.rollback() | ||
| return jsonify({"message": "This dashboard is already assigned to that organization."}), 409 |
There was a problem hiding this comment.
As we don't have serializers classes (at least not that I'm aware of), I'm also fine with leaving it as it's. Although I agree that this is a serializer validation, in Django realms, we don't have easy or no-brainer ways of doing that within Flask without installing third party apps like, for example, flask-serialize. I'm not advising us to do that hehehe
|
@imenattatra thank you . I took a look. I dont think there is much more information to display at the moment to be honest. I guess at some point in the future we will want to see from the orga side i.e. which orga has whcih sub dashboards. But at the moment we dont have bespoke client template dashboard so it could be added later. |
The slug names the org holding sub-dashboard templates, which only Redash Global reads. Keeping it in redash/settings gave the main app a constant it never uses and blurred the boundary between the two apps. It's now also required rather than silently defaulted.
The Redash Global tests lived under tests/ and inherited from BaseTestCase, so they were coupled to the main suite's unittest base class and scattered across files named after the app rather than owned by it. Rewriting them as pytest fixtures inside redash_global/ makes the app's tests self-contained: the global app's tests now sit next to the code they exercise and share nothing with the main suite but the model factories and the test database. - Add redash_global/tests/ as the app's own test package - Replace BaseTestCase/GlobalBaseTestCase with fixtures - Set GLOBAL_SECRET_KEY and TEMPLATE_ORG_SLUG in the new package's __init__, and import tests/__init__ from it, because both settings modules read the environment at import time and Redash Global runs against the main Redash database, so it needs the same test environment - Stub flask_babel._ in the new conftest with pytest.MonkeyPatch, not unittest.mock - Point CI, bin/docker-entrypoint and .coveragerc at the new directory, as both pass explicit test paths and would have silently skipped it - Drop TEMPLATE_ORG_SLUG from tests/__init__, now that nothing under tests/ starts the global app - Disable the global rate limiter in the app fixture
@helenalebreton As agreed with the team, the next step is the backend for the deploy/redeploy as explained in detail here in this comment https://github.com/metr-systems/backlog/issues/4769#issuecomment-5121270418 |
thiagogds
left a comment
There was a problem hiding this comment.
Since you're using Claude, please also update the claude.md file with the reviews you got in in this PR, like always using pytest, and create factories for models for example. This will save us time next time
|
|
||
| @pytest.fixture | ||
| def url(dashboard): | ||
| return "/global-api/sub-dashboards/{}/assignments".format(dashboard.id) |
| def create(org): | ||
| assignment = SubDashboardAssignment(dashboard_id=dashboard.id, organization_id=org.id) | ||
| db.session.add(assignment) | ||
| db.session.commit() | ||
| return assignment |
There was a problem hiding this comment.
Redash already have a Factory pattern, you should use that too
| def create(name="Template A", **kwargs): | ||
| dashboard = factory.create_dashboard(org=template_org, name=name, **kwargs) | ||
| db.session.commit() | ||
| return dashboard |
There was a problem hiding this comment.
Same thing about the Factory pattern
berinhard
left a comment
There was a problem hiding this comment.
I'm adding my review as a simple comment one, as I'm only agreeing with @thiagogds points
Closes https://github.com/metr-systems/backlog/issues/4783
AI Disclaimer: Yes claude to plan and assist implementing and helped me shape up this PR description
Summary
This PR adds the ability to manage sub-dashboards in the Redash Global admin:
- browse the dashboards that live in the template org (se_template in production and se_staging_template in staging
- assign or unassign them to individual organizations.
A lot of decisions in this PR will make it easy for the next 2 PRs to be done and this is why it is quite big. Let me know if you need to discuss anything.
Code Strategy
Patterns reused from main Redash
Structure choices
Endpoint formats
Edge cases
QA
How did you test it? Keep your future self in mind to help debug things later.
@helenalebreton this is minor, but can you please also check what should be the default (home) page we access on https://global-dashboard.staging.metr.systems/ ? we can also just redirect it to the subdashbaords or composed dashboard page directly.
Also, please let me know whenever you have time so that we can talk about details, i know this version shows minimum information, either for the list of dashboards or for the assignments but we can easily add later on any information you see that it needs to be there.