Skip to content

Keep track of provenance in the metadata viewer, and allow user to override values - #2083

Draft
jdbocarsly wants to merge 9 commits into
mainfrom
jdb/metadata-provenance
Draft

jdbocarsly wants to merge 9 commits into
mainfrom
jdb/metadata-provenance

Conversation

@jdbocarsly

@jdbocarsly jdbocarsly commented Sep 6, 2026

Copy link
Copy Markdown
Member

The metadata viewer currently nicely displays the metadata available on a block, which is generally set during file read. This PR adds the ability for the block developer to set how the metadata should be determined (e.g., from the data file vs. inherited from the sample vs. supplied by the user). The user can also override any values by clicking on it in the metadata viewer, and that metadata is tracked.

This PR will also probably hide metadata by default, and fix an issue where hiding/showing the metadata causes the screen to jump positions (not complete yet).

Some screenshots of the new behavior (demonstrated with the private SQUID block):
temp

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.91667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.70%. Comparing base (149e6bb) to head (00c28e1).

Files with missing lines Patch % Lines
pydatalab/src/pydatalab/blocks/base.py 96.15% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2083      +/-   ##
==========================================
+ Coverage   80.50%   80.70%   +0.20%     
==========================================
  Files          84       85       +1     
  Lines        7909     8003      +94     
==========================================
+ Hits         6367     6459      +92     
- Misses       1542     1544       +2     
Files with missing lines Coverage Δ
pydatalab/src/pydatalab/blocks/metadata.py 100.00% <100.00%> (ø)
pydatalab/src/pydatalab/models/blocks.py 100.00% <100.00%> (ø)
pydatalab/src/pydatalab/blocks/base.py 95.78% <96.15%> (+0.07%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress

cypress Bot commented Sep 6, 2026

Copy link
Copy Markdown

datalab    Run #5709

Run Properties:  status check passed Passed #5709  •  git commit 69468f8a0f ℹ️: Merge 00c28e1e341a692f68229479a3ed40268eae5091 into 149e6bbd6a30cd72d081b1390066...
Project datalab
Branch Review jdb/metadata-provenance
Run status status check passed Passed #5709
Run duration 18m 08s
Commit git commit 69468f8a0f ℹ️: Merge 00c28e1e341a692f68229479a3ed40268eae5091 into 149e6bbd6a30cd72d081b1390066...
Committer Josh Bocarsly
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 188
View all changes introduced in this branch ↗︎

A block's metadata is rarely all from one place: a sample mass written by the
instrument, a molar mass derived from the sample's formula, either of them
corrected by hand. Which of those a value came from is worth knowing, and it
decides what should happen to that value later.

So a field is bound to a source, and it is the binding that persists rather than
the value. Bound to a source, the value is re-read on every render, and follows
the file if the file is replaced or the sample if the sample is edited. Bound to
the user, it is what they gave and nothing recomputes it. Bound to nothing, the
sources are tried in order -- a guess, so it is made again each time and may
change its mind when a better source turns up.

The rule underneath: the block's own choices stay open, the user's are kept.
Clearing a field by hand is a choice, and says there is no good value for this
one, so it survives the next render rather than being filled in again from the
file. That is why it cannot be represented as an absent value, and why a binding
has to be a thing in its own right.

A block supplies `metadata_sources`; everything else is here. Every source is
read whether or not it wins, so the interface can say where a number came from
and offer the alternatives, and one event covers binding any field to any of
them.

The item schemas move because DataBlockResponse is embedded in them.
A metadata field the block tracks now renders as its value plus where that value
came from, and right-clicking it offers what can be done: override it by hand,
take it from one of the other sources, set it to empty, or hand it back to the
block to decide.

The menu only offers what the block says it has, so a source with nothing for
this field never appears, and "choose automatically" only shows up once there is
a choice to undo. A tracked field is listed even when it is empty, since an
empty one is exactly the one somebody needs to reach.

The provenance sits beside the value rather than replacing it -- quiet enough to
read past, with the explanation on hover. A value somebody typed is the one worth
being able to pick out at a glance, so that is the one that is marked.

An emptied box sends a value of the user's rather than no binding at all, which
is what makes "there is no good value for this" survive the next render.
"edited", in yellow, is how the rest of datalab says a change has not been saved
yet. On a metadata value it meant something else entirely -- that the number came
from a person rather than from the file -- and read as a warning about work in
progress.

It now says "user supplied", in a blue that carries no such implication.
A right-click was a strange thing to require of the one control in the panel
that does anything; a plain click opens the menu now, and a right-click still
does too for anyone who reaches for one.

An edit gains a save and a cancel beside the box, so backing out is something
you can see rather than something you have to know. Clicking away cancels as
well, on the grounds that abandoning an edit should never be the thing that
writes a value.

Both of those needed the same fix, which the tests found: a click on a menu
option, or on the save or cancel button, was bubbling back to the field and
reopening the menu the click had just closed.
"Read from the file" is only half of what somebody wants to know, and the half
they already assumed. A block can now say what to call each of its sources, so
the note reads "Supplied by NiCl2btd_MT_ZFC_210707_MPMS-XL.rso.dat", and falls
back to the source's own name where a block has nothing better to offer.

A value of the user's says what was done rather than what will happen to it --
overwritten, or set to blank -- since the consequence is already in the menu.

The help cursor goes: a note that explains itself on hover does not need to
advertise that it will.
A binding is somebody deciding that a value should not be worked out the usual
way. Six months later that is exactly the thing nobody can remember, so the
choice now carries who made it and when, and the note on the value says so:
"Value overwritten by Ada Lovelace on 5 Sep 2026".

Choosing which source to take a value from is attributed the same way, since it
is the same kind of decision -- somebody looked at a number and said use that one.

There is not always anyone to name. A binding made by a script, or before this
was recorded, keeps the time or says nothing at all rather than failing; the
interface says only what it knows.
`to_web` rebuilds the block's errors from what its plot functions report, which
threw away anything an event had recorded on the way there. A control that
failed therefore did nothing and said nothing about why -- the worst of both,
and a slow thing to debug from the outside.

Event errors are now kept on the instance for the life of the request and
reported alongside the plots', while the stored errors are still cleared by a
clean render, so nothing sticks around from last time.

Also says in `metadata_sources` what its callers already required: it is asked
what the sources are when a binding is set, which happens before anything has
been rendered.
Emptying a field made it jump to the bottom of the panel and filling it in
brought it back, because the list was built from the values first and the empty
tracked fields appended afterwards. A field that moves when you change it is
hard to work with and easy to lose.

The order now comes from `metadata_fields`, which the block writes out in the
order its metadata model declares them and which carries every field whether or
not it has a value -- `metadata` cannot serve, since a value of None is dropped
from it altogether. Blocks that track nothing keep the order they had.
@jdbocarsly
jdbocarsly force-pushed the jdb/metadata-provenance branch from 40baaba to e5f26ce Compare September 8, 2026 15:37
A code review found several holes in the provenance, the worst of which made it
worthless: `metadata_bindings` was loadable from the web, so anyone who could
update a block could POST a binding claiming a colleague had set a value, and
the note on the field would say so. It is server-authoritative now, like the
other three metadata slots -- only `set_metadata_source` writes it, and only
that path stamps who and when. A binding that is not a mapping is ignored rather
than taking the whole resolution down with it.

Values are coerced by validating them rather than by assigning them, since a
model only checks assignments when it asks to. Without this, a metadata model
that did not happen to enable `validate_assignment` got no checking at all: a
file offering "heavy" for a mass had it stored verbatim, and a user typing "99"
had it kept as a string.

Setting a source now resolves before returning. `to_web` only runs the plot
functions, so a block whose plots do not resolve -- or which has no plots --
answered the request with the value and the source the field had beforehand,
and the click appeared to do nothing.

A source may no longer be called "user" or "auto". Those are the two answers the
event gives itself, so a block naming one of them could never bind to it, and
asking for it would silently blank the field.

Three smaller ones: clicking a field no longer swallows the click, so opening
one menu closes any other; a binding that records a time but nobody to name now
says "by user" rather than crediting the timestamp; and the derived provenance is
stripped before a block is sent, since the server rebuilds it and refuses to
load it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant