Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pydatalab/schemas/cell.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@
"title": "Block Id",
"type": "string"
},
"created_at": {
"title": "Created At",
"type": "string",
"format": "date-time"
},
"item_id": {
"title": "Item Id",
"type": "string"
Expand Down
5 changes: 5 additions & 0 deletions pydatalab/schemas/equipment.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@
"title": "Block Id",
"type": "string"
},
"created_at": {
"title": "Created At",
"type": "string",
"format": "date-time"
},
"item_id": {
"title": "Item Id",
"type": "string"
Expand Down
5 changes: 5 additions & 0 deletions pydatalab/schemas/sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@
"title": "Block Id",
"type": "string"
},
"created_at": {
"title": "Created At",
"type": "string",
"format": "date-time"
},
"item_id": {
"title": "Item Id",
"type": "string"
Expand Down
5 changes: 5 additions & 0 deletions pydatalab/schemas/startingmaterial.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@
"title": "Block Id",
"type": "string"
},
"created_at": {
"title": "Created At",
"type": "string",
"format": "date-time"
},
"item_id": {
"title": "Item Id",
"type": "string"
Expand Down
4 changes: 4 additions & 0 deletions pydatalab/src/pydatalab/blocks/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import functools
import pprint
import random
Expand Down Expand Up @@ -183,6 +184,9 @@ def __init__(
**self.defaults,
}

if "created_at" not in self.data:
self.data["created_at"] = datetime.datetime.now(tz=datetime.timezone.utc)

# convert ObjectId file_ids to string to make handling them easier when sending to and from web
if "file_id" in self.data:
self.data["file_id"] = str(self.data["file_id"])
Expand Down
5 changes: 5 additions & 0 deletions pydatalab/src/pydatalab/models/blocks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

from pydantic import BaseModel, Field

from pydatalab.models.utils import JSON_ENCODERS, PyObjectId
Expand All @@ -17,6 +19,9 @@ class DataBlockResponse(BaseModel):
block_id: str
"""A shorthand random ID for the block."""

created_at: datetime.datetime | None = None
"""When the block was created, in UTC."""

item_id: str | None = None
"""The item that the block is attached to, if any."""

Expand Down
3 changes: 2 additions & 1 deletion pydatalab/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import time
import typing

import tomlkit
from invoke import Collection, task

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -173,6 +172,8 @@ def install(_, dev=True):

"""

import tomlkit

plugin_cfg = PLUGINS_TOML_PATH

deps: list[str] = []
Expand Down
66 changes: 66 additions & 0 deletions webapp/src/components/SampleInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@

<TableOfContents :item_id="item_id" :information-sections="tableOfContentsSections" />
<SynthesisInformation class="mt-3" :item_id="item_id" />

<div id="sample-stages" class="mt-3">
<SampleStagesTimeline :stages="sampleStages" @stage-click="scrollToBlock" />
</div>
</div>
</template>

Expand All @@ -78,6 +82,7 @@ import SynthesisInformation from "@/components/SynthesisInformation";
import SubstanceInformation from "@/components/SubstanceInformation";
import TableOfContents from "@/components/TableOfContents";
import ItemRelationshipVisualization from "@/components/ItemRelationshipVisualization";
import SampleStagesTimeline from "@/components/SampleStagesTimeline";

export default {
components: {
Expand All @@ -86,6 +91,7 @@ export default {
SubstanceInformation,
TableOfContents,
ItemRelationshipVisualization,
SampleStagesTimeline,
FormattedRefcode,
ToggleableCollectionFormGroup,
ToggleableCreatorsFormGroup,
Expand All @@ -106,6 +112,7 @@ export default {
{ title: "Sample Information", targetID: "sample-information" },
{ title: "Substance Information", targetID: "substance-information" },
{ title: "Synthesis Information", targetID: "synthesis-information" },
{ title: "Sample Stages", targetID: "sample-stages" },
],
};
},
Expand All @@ -128,6 +135,65 @@ export default {
possibleItemStatuses() {
return this.schema?.attributes?.schema?.definitions?.ItemStatus?.enum;
},
sampleStages() {
const item = this.item || {};
const blocks = item.blocks_obj || {};
const displayOrder = item.display_order || Object.keys(blocks);
const baseTimestamp = item.date ? new Date(item.date).getTime() : Date.now();

return displayOrder
.map((blockId, index) => {
const block = blocks[blockId];

if (!block) {
return null;
}

const blocktype = block.blocktype || "Block";
const title = block.title || blocktype;
const rawTimestamp =
block.created_at || block.createdAt || block.date_created || block.timestamp;
const timestamp = rawTimestamp
? rawTimestamp
: new Date(baseTimestamp + index * 10 * 60 * 1000).toISOString();
const previousBlockId = displayOrder[index - 1];
const previousBlock = previousBlockId ? blocks[previousBlockId] : null;
const transitionLabel = previousBlock
? `from ${previousBlock.blocktype || "Block"} to ${blocktype}`
: "initial stage";

return {
id: blockId,
block_id: blockId,
blocktype,
full_name: title,
title,
name: title,
timestamp,
detail: block.freeform_comment || "",
metadata: block.metadata || null,
transition_label: transitionLabel,
};
})
.filter(Boolean);
},
},
methods: {
scrollToBlock(stage) {
const blockId = stage?.block_id || stage?.id;
if (!blockId || typeof document === "undefined") {
return;
}

const target = document.getElementById(blockId);
if (!target) {
return;
}

const headerOffset = 88;
const top = target.getBoundingClientRect().top + window.scrollY - headerOffset;
window.scrollTo({ top, behavior: "smooth" });
},
},
};
</script>
Loading
Loading