Skip to content

Commit

Permalink
Show Thumbnails in Dataset Table (#7479)
Browse files Browse the repository at this point in the history
* WIP: Show Thumbnails in Dataset Table

* WIP: query

* use layer names

* typecheck,snapshots

* order layer names

* fix duplicate layer names, fix string array parsing

* ordering, make linter happy

* snapshots

* integrate type column into thumbnail

* lint

* slightly changed tag colors, more escaping, new folder+dataset images

* dark mode, changelog

* test naming

* Update frontend/javascripts/types/api_flow_types.ts

Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com>

* Update frontend/javascripts/types/api_flow_types.ts

Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com>

* Update frontend/javascripts/dashboard/advanced_dataset/dataset_table.tsx

Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com>

* pr feedback, format

---------

Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com>
  • Loading branch information
fm3 and philippotto committed Dec 13, 2023
1 parent f59ec83 commit 1140374
Show file tree
Hide file tree
Showing 16 changed files with 417 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added support for S3-compliant object storage services (e.g. MinIO) as a storage backend for remote datasets. [#7453](https://github.com/scalableminds/webknossos/pull/7453)
- Added support for blosc compressed N5 datasets. [#7465](https://github.com/scalableminds/webknossos/pull/7465)
- Added route for triggering the compute segment index worker job. [#7471](https://github.com/scalableminds/webknossos/pull/7471)
- Added thumbnails to the dashboard dataset list. [#7479](https://github.com/scalableminds/webknossos/pull/7479)

### Changed
- Improved loading speed of the annotation list. [#7410](https://github.com/scalableminds/webknossos/pull/7410)
Expand Down
35 changes: 29 additions & 6 deletions app/models/dataset/Dataset.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ case class Dataset(_id: ObjectId,
logoUrl: Option[String],
sortingKey: Instant = Instant.now,
details: Option[JsObject] = None,
tags: Set[String] = Set.empty,
tags: List[String] = List.empty,
created: Instant = Instant.now,
isDeleted: Boolean = false)
extends FoxImplicits {
Expand All @@ -75,7 +75,9 @@ case class DatasetCompactInfo(
lastUsedByUser: Instant,
status: String,
tags: List[String],
isUnreported: Boolean
isUnreported: Boolean,
colorLayerNames: List[String],
segmentationLayerNames: List[String],
)

object DatasetCompactInfo {
Expand Down Expand Up @@ -133,7 +135,7 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
r.logourl,
Instant.fromSql(r.sortingkey),
details,
parseArrayLiteral(r.tags).toSet,
parseArrayLiteral(r.tags).sorted,
Instant.fromSql(r.created),
r.isdeleted
)
Expand Down Expand Up @@ -262,7 +264,9 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
) AS isEditable,
COALESCE(lastUsedTimes.lastUsedTime, ${Instant.zero}),
d.status,
d.tags
d.tags,
cl.names AS colorLayerNames,
sl.names AS segmentationLayerNames
FROM
(SELECT $columns FROM $existingCollectionName WHERE $selectionPredicates $limitQuery) d
JOIN webknossos.organizations o
Expand All @@ -271,9 +275,26 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
ON u._id = $requestingUserIdOpt
LEFT JOIN webknossos.dataSet_lastUsedTimes lastUsedTimes
ON lastUsedTimes._dataSet = d._id AND lastUsedTimes._user = u._id
LEFT JOIN (SELECT _dataset, ARRAY_AGG(name ORDER BY name) AS names FROM webknossos.dataSet_layers WHERE category = 'color' GROUP BY _dataset) cl
ON d._id = cl._dataset
LEFT JOIN (SELECT _dataset, ARRAY_AGG(name ORDER BY name) AS names FROM webknossos.dataSet_layers WHERE category = 'segmentation' GROUP BY _dataset) sl
ON d._id = sl._dataset
"""
rows <- run(
query.as[(ObjectId, String, String, ObjectId, Boolean, String, Instant, Boolean, Instant, String, String)])
query.as[
(ObjectId,
String,
String,
ObjectId,
Boolean,
String,
Instant,
Boolean,
Instant,
String,
String,
String,
String)])
} yield
rows.toList.map(
row =>
Expand All @@ -290,6 +311,8 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
status = row._10,
tags = parseArrayLiteral(row._11),
isUnreported = unreportedStatusList.contains(row._10),
colorLayerNames = parseArrayLiteral(row._12),
segmentationLayerNames = parseArrayLiteral(row._13)
))

private def buildSelectionPredicates(isActiveOpt: Option[Boolean],
Expand Down Expand Up @@ -513,7 +536,7 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
${d.inboxSourceHash}, $defaultViewConfiguration, $adminViewConfiguration,
${d.description}, ${d.displayName}, ${d.isPublic}, ${d.isUsable},
${d.name}, ${d.scale}, ${d.status.take(1024)},
${d.sharingToken}, ${d.sortingKey}, ${d.details}, ${d.tags.toList},
${d.sharingToken}, ${d.sortingKey}, ${d.details}, ${d.tags},
${d.created}, ${d.isDeleted})
""".asUpdate)
} yield ()
Expand Down
42 changes: 26 additions & 16 deletions app/utils/sql/SqlEscaping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,36 @@ trait SqlEscaping {
}
}

protected def enumArrayLiteral(elements: List[Enumeration#Value]): String = {
val commaSeparated = elements.map(e => s""""$e"""").mkString(",")
s"'{$commaSeparated}'"
}

protected def parseArrayLiteral(literal: String): List[String] = {
val trimmed = literal.drop(1).dropRight(1)
if (trimmed.isEmpty)
List.empty
protected def parseArrayLiteral(literal: String): List[String] =
if (literal == null) List.empty
else {
val split = trimmed.split(",", -1).toList.map(unescapeInArrayLiteral)
split.map { item =>
if (item.startsWith("\"") && item.endsWith("\"")) {
item.drop(1).dropRight(1)
} else item
val trimmed = literal.drop(1).dropRight(1)
if (trimmed.isEmpty)
List.empty
else {
// Removing the escaped quotes to split at commas not surrounded by quotes
// Splitting *the original string* at split positions obtained from matching there
val withoutEscapedQuotes = trimmed.replace("\\\"", "__")
val splitPositions: List[Int] =
",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)".r.findAllMatchIn(withoutEscapedQuotes).map(_.start).toList.sorted
val split = splitAtPositions(splitPositions, trimmed)
split.map(unescapeInArrayLiteral)
}
}

// Split a string at specified positions. Drop 1 character at every split
private def splitAtPositions(positions: List[Int], aString: String): List[String] = positions match {
case pos :: remainingPositions =>
aString.substring(0, pos) :: splitAtPositions(remainingPositions.map(_ - pos - 1), aString.substring(pos + 1))
case Nil => List(aString)
}

protected def unescapeInArrayLiteral(aString: String): String =
aString.replaceAll("""\\"""", """"""").replaceAll("""\\,""", ",")
private def unescapeInArrayLiteral(aString: String): String = {
val withUnescapedQuotes =
aString.replace("\\\"", """"""").replace("\\,", ",").replace("\\\\", "\\")
if (withUnescapedQuotes.startsWith("\"") && withUnescapedQuotes.endsWith("\"")) {
withUnescapedQuotes.drop(1).dropRight(1)
} else withUnescapedQuotes
}

}
84 changes: 52 additions & 32 deletions frontend/javascripts/dashboard/advanced_dataset/dataset_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const { Column } = Table;
const typeHint: RowRenderer[] = [];
const useLruRank = true;

const THUMBNAIL_SIZE = 100;

type Props = {
datasets: Array<APIDatasetCompact>;
subfolders: FolderItem[];
Expand Down Expand Up @@ -278,24 +280,48 @@ class DatasetRenderer {
return <FileOutlined style={{ fontSize: "18px" }} />;
}
renderNameColumn() {
const selectedLayerName: string | null =
this.data.colorLayerNames[0] || this.data.segmentationLayerNames[0];
const imgSrc = selectedLayerName
? `/api/datasets/${this.data.owningOrganization}/${
this.data.name
}/layers/${selectedLayerName}/thumbnail?w=${2 * THUMBNAIL_SIZE}&h=${2 * THUMBNAIL_SIZE}`
: "/assets/images/inactive-dataset-thumbnail.svg";
const iconClassName = selectedLayerName ? "" : " icon-thumbnail";
return (
<>
<Link
to={`/datasets/${this.data.owningOrganization}/${this.data.name}/view`}
title="View Dataset"
className="incognito-link"
>
{this.data.name}
<img
src={imgSrc}
className={`dataset-table-thumbnail ${iconClassName}`}
style={{ width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE }}
alt=""
/>
</Link>
<br />
<div className="dataset-table-name-container">
<Link
to={`/datasets/${this.data.owningOrganization}/${this.data.name}/view`}
title="View Dataset"
className="incognito-link dataset-table-name"
>
{this.data.name}
</Link>

{this.datasetTable.props.context.globalSearchQuery != null ? (
<BreadcrumbsTag parts={this.datasetTable.props.context.getBreadcrumbs(this.data)} />
) : null}
{this.renderTags()}
{this.datasetTable.props.context.globalSearchQuery != null ? (
<>
<br />
<BreadcrumbsTag parts={this.datasetTable.props.context.getBreadcrumbs(this.data)} />
</>
) : null}
</div>
</>
);
}
renderTagsColumn() {
renderTags() {
return this.data.isActive ? (
<DatasetTags
dataset={this.data}
Expand Down Expand Up @@ -335,14 +361,20 @@ class FolderRenderer {
getRowKey() {
return this.data.key;
}
renderTypeColumn() {
return <FolderOpenOutlined style={{ fontSize: "18px" }} />;
}
renderNameColumn() {
return this.data.name;
}
renderTagsColumn() {
return null;
return (
<>
<img
src={"/assets/images/folder-thumbnail.svg"}
className="dataset-table-thumbnail icon-thumbnail"
style={{ width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE }}
alt=""
/>
<div className="dataset-table-name-container">
<span className="incognito-link dataset-table-name">{this.data.name}</span>
</div>
</>
);
}
renderCreationDateColumn() {
return null;
Expand Down Expand Up @@ -698,33 +730,17 @@ class DatasetTable extends React.PureComponent<Props, State> {
},
}}
>
<Column
width={70}
title="Type"
key="type"
render={(__, renderer: RowRenderer) => renderer.renderTypeColumn()}
/>
<Column
title="Name"
dataIndex="name"
key="name"
width={280}
sorter={Utils.localeCompareBy<RowRenderer>(
typeHint,
(rowRenderer) => rowRenderer.data.name,
)}
sortOrder={sortedInfo.columnKey === "name" ? sortedInfo.order : undefined}
render={(_name: string, renderer: RowRenderer) => renderer.renderNameColumn()}
/>
<Column
title="Tags"
dataIndex="tags"
key="tags"
sortOrder={sortedInfo.columnKey === "name" ? sortedInfo.order : undefined}
render={(_tags: Array<string>, rowRenderer: RowRenderer) =>
rowRenderer.renderTagsColumn()
}
/>
<Column
width={180}
title="Creation Date"
Expand Down Expand Up @@ -803,7 +819,11 @@ export function DatasetTags({
/>
))}
{dataset.isEditable ? (
<EditableTextIcon icon={<PlusOutlined />} onChange={_.partial(editTagFromDataset, true)} />
<EditableTextIcon
icon={<PlusOutlined />}
onChange={_.partial(editTagFromDataset, true)}
label="Add Tag"
/>
) : null}
</div>
);
Expand Down Expand Up @@ -896,7 +916,7 @@ function BreadcrumbsTag({ parts: allParts }: { parts: string[] | null }) {

return (
<Tooltip title={`This dataset is located in ${formatPath(allParts)}.`}>
<Tag>
<Tag style={{ marginTop: "5px" }}>
<FolderOpenOutlined />
{formatPath(parts)}
</Tag>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS
import type {
APIDatasetId,
APIDatasetCompact,
APIDatasetCompactWithoutStatus,
APIDatasetCompactWithoutStatusAndLayerNames,
FolderItem,
} from "types/api_flow_types";
import { DatasetUpdater, getDatastores, triggerDatasetCheck } from "admin/admin_rest_api";
Expand Down Expand Up @@ -45,7 +45,7 @@ export type DatasetCollectionContextValue = {
setGlobalSearchQuery: (val: string | null) => void;
searchRecursively: boolean;
setSearchRecursively: (val: boolean) => void;
getBreadcrumbs: (dataset: APIDatasetCompactWithoutStatus) => string[] | null;
getBreadcrumbs: (dataset: APIDatasetCompactWithoutStatusAndLayerNames) => string[] | null;
getActiveSubfolders: () => FolderItem[];
showCreateFolderPrompt: (parentFolderId: string) => void;
queries: {
Expand Down Expand Up @@ -160,7 +160,7 @@ export default function DatasetCollectionContextProvider({
await updateDatasetMutation.mutateAsync([id, updater]);
}

const getBreadcrumbs = (dataset: APIDatasetCompactWithoutStatus) => {
const getBreadcrumbs = (dataset: APIDatasetCompactWithoutStatusAndLayerNames) => {
if (folderHierarchyQuery.data?.itemById == null) {
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/javascripts/libs/format_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const COLOR_MAP: Array<string> = [
"#575AFF",
"#8086FF",
"#2A0FC6",
"#37C6DC",
"#F61A76",
"#40bfd2",
"#b92779",
"#FF7BA6",
"#FF9364",
"#750790",
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const localStoragePersister = createSyncStoragePersister({
storage: UserLocalStorage,
serialize: (data) => compress(JSON.stringify(data)),
deserialize: (data) => JSON.parse(decompress(data) || "{}"),
key: "query-cache-v2",
key: "query-cache-v3",
});

async function loadActiveUser() {
Expand Down
12 changes: 10 additions & 2 deletions frontend/javascripts/oxalis/view/components/editable_text_icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";

type Props = {
icon: React.ReactElement;
label?: string;
onChange: (value: string, event: React.SyntheticEvent<HTMLInputElement>) => void;
};

Expand Down Expand Up @@ -57,13 +58,20 @@ class EditableTextIcon extends React.PureComponent<Props, State> {
<Button
size="small"
icon={this.props.icon}
style={{ height: 22, width: 22 }}
style={{
height: 22,
width: this.props.label ? "initial" : 22,
fontSize: "12px",
color: "#7c7c7c",
}}
onClick={() =>
this.setState({
isEditing: true,
})
}
/>
>
{this.props.label ? <span style={{ marginLeft: 0 }}>{this.props.label}</span> : null}
</Button>
);
}
}
Expand Down
Loading

0 comments on commit 1140374

Please sign in to comment.