Skip to content

Commit

Permalink
Add tags for datasets (#5832)
Browse files Browse the repository at this point in the history
* Add tags for datasets

* update schema version

* add tag column to dataset table

* add filtering and persistence for dataset tags

* unify tag handling for datasets and explorative annotations view

* adjust version of migration

* adjust revision of dataset migration

* ensure qdataset updates to be in fixed order

* Add tags for datasets

* undo accidental changes due to merging

* Update frontend/javascripts/dashboard/dataset/dataset_cache_provider.js

Co-authored-by: Philipp Otto <[email protected]>

* update version in schema.sql manually

* fix flow

Co-authored-by: Michael Büßemeyer <[email protected]>
Co-authored-by: MichaelBuessemeyer <[email protected]>
Co-authored-by: Michael Büßemeyer <[email protected]>
Co-authored-by: Philipp Otto <[email protected]>
  • Loading branch information
5 people committed Dec 22, 2021
1 parent 71b5c2c commit deffe5a
Show file tree
Hide file tree
Showing 19 changed files with 316 additions and 80 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
[Commits](https://github.com/scalableminds/webknossos/compare/21.11.0...HEAD)

### Added
-
- Added tagging support for datasets. [#5832](https://github.com/scalableminds/webknossos/pull/5832)

### Changed
-
Expand Down
1 change: 1 addition & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).

### Postgres Evolutions:
- [078-annotation-layers.sql](conf/evolutions/078-annotation-layers.sql)
- [079-add-dataset-tags.sql](conf/evolutions/079-add-dataset-tags.sql)
10 changes: 5 additions & 5 deletions app/controllers/DataSetController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class DataSetController @Inject()(userService: UserService,
((__ \ 'description).readNullable[String] and
(__ \ 'displayName).readNullable[String] and
(__ \ 'sortingKey).readNullable[Long] and
(__ \ 'isPublic).read[Boolean]).tupled
(__ \ 'isPublic).read[Boolean] and
(__ \ 'tags).read[List[String]]).tupled

@ApiOperation(hidden = true, value = "")
def removeFromThumbnailCache(organizationName: String, dataSetName: String): Action[AnyContent] =
Expand Down Expand Up @@ -269,7 +270,7 @@ class DataSetController @Inject()(userService: UserService,
def update(organizationName: String, dataSetName: String): Action[JsValue] = sil.SecuredAction.async(parse.json) {
implicit request =>
withJsonBodyUsing(dataSetPublicReads) {
case (description, displayName, sortingKey, isPublic) =>
case (description, displayName, sortingKey, isPublic, tags) =>
for {
dataSet <- dataSetDAO
.findOneByNameAndOrganization(dataSetName, request.identity._organization) ?~> notFoundMessage(
Expand All @@ -281,14 +282,13 @@ class DataSetController @Inject()(userService: UserService,
displayName,
sortingKey.getOrElse(dataSet.created),
isPublic)
_ <- dataSetDAO.updateTags(dataSet._id, tags)
updated <- dataSetDAO.findOneByNameAndOrganization(dataSetName, request.identity._organization)
_ = analyticsService.track(ChangeDatasetSettingsEvent(request.identity, updated))
organization <- organizationDAO.findOne(updated._organization)(GlobalAccessContext)
dataStore <- dataSetService.dataStoreFor(updated)
js <- dataSetService.publicWrites(updated, Some(request.identity), organization, dataStore)
} yield {
Ok(Json.toJson(js))
}
} yield Ok(Json.toJson(js))
}
}

Expand Down
14 changes: 12 additions & 2 deletions app/models/binary/DataSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ case class DataSet(
logoUrl: Option[String],
sortingKey: Long = System.currentTimeMillis(),
details: Option[JsObject] = None,
tags: Set[String] = Set.empty,
created: Long = System.currentTimeMillis(),
isDeleted: Boolean = false
) extends FoxImplicits {
Expand Down Expand Up @@ -104,6 +105,7 @@ class DataSetDAO @Inject()(sqlClient: SQLClient,
r.logourl,
r.sortingkey.getTime,
details,
parseArrayTuple(r.tags).toSet,
r.created.getTime,
r.isdeleted
)
Expand Down Expand Up @@ -230,6 +232,13 @@ class DataSetDAO @Inject()(sqlClient: SQLClient,
} yield ()
}

def updateTags(id: ObjectId, tags: List[String])(implicit ctx: DBAccessContext): Fox[Unit] =
for {
_ <- assertUpdateAccess(id)
_ <- run(
sqlu"update webknossos.datasets set tags = '#${writeArrayTuple(tags.map(sanitize))}' where _id = ${id.id}")
} yield ()

def updateAdminViewConfiguration(datasetId: ObjectId, configuration: DataSetViewConfiguration)(
implicit ctx: DBAccessContext): Fox[Unit] =
for {
Expand All @@ -254,15 +263,16 @@ class DataSetDAO @Inject()(sqlClient: SQLClient,
for {
_ <- run(
sqlu"""insert into webknossos.dataSets(_id, _dataStore, _organization, _publication, _uploader, inboxSourceHash, defaultViewConfiguration, adminViewConfiguration, description, displayName,
isPublic, isUsable, name, scale, status, sharingToken, sortingKey, details, created, isDeleted)
isPublic, isUsable, name, scale, status, sharingToken, sortingKey, details, tags, created, isDeleted)
values(${d._id.id}, ${d._dataStore}, ${d._organization.id}, #${optionLiteral(d._publication.map(_.id))},
#${optionLiteral(d._uploader.map(_.id))},
#${optionLiteral(d.inboxSourceHash.map(_.toString))}, #${optionLiteral(
defaultViewConfiguration.map(sanitize))}, #${optionLiteral(adminViewConfiguration.map(sanitize))},
${d.description}, ${d.displayName}, ${d.isPublic}, ${d.isUsable},
${d.name}, #${optionLiteral(d.scale.map(s => writeScaleLiteral(s)))}, ${d.status
.take(1024)}, ${d.sharingToken}, ${new java.sql.Timestamp(d.sortingKey)}, #${optionLiteral(
details.map(sanitize))}, ${new java.sql.Timestamp(d.created)}, ${d.isDeleted})
details.map(sanitize))}, '#${writeArrayTuple(d.tags.toList.map(sanitize))}', ${new java.sql.Timestamp(
d.created)}, ${d.isDeleted})
""")
} yield ()
}
Expand Down
1 change: 1 addition & 0 deletions app/models/binary/DataSetService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO,
"isUnreported" -> Json.toJson(isUnreported(dataSet)),
"isForeign" -> dataStore.isForeign,
"jobsEnabled" -> jobsEnabled,
"tags" -> dataSet.tags
)
}
}
11 changes: 11 additions & 0 deletions conf/evolutions/079-add-dataset-tags.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
START TRANSACTION;

DROP VIEW webknossos.dataSets_;

ALTER TABLE webknossos.dataSets ADD COLUMN tags VARCHAR(256)[] NOT NULL DEFAULT '{}';

CREATE VIEW webknossos.dataSets_ AS SELECT * FROM webknossos.dataSets WHERE NOT isDeleted;

UPDATE webknossos.releaseInformation SET schemaVersion = 79;

COMMIT TRANSACTION;
11 changes: 11 additions & 0 deletions conf/evolutions/reversions/079-add-dataset-tags.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
START TRANSACTION;

DROP VIEW webknossos.dataSets_;

ALTER TABLE webknossos.dataSets DROP COLUMN tags;

CREATE VIEW webknossos.dataSets_ AS SELECT * FROM webknossos.dataSets WHERE NOT isDeleted;

UPDATE webknossos.releaseInformation SET schemaVersion = 78;

COMMIT TRANSACTION;
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function NewAnnotationLink({

type Props = {
dataset: APIMaybeUnimportedDataset,
updateDataset: APIDatasetId => Promise<void>,
reloadDataset: APIDatasetId => Promise<void>,
};

type State = {
Expand Down Expand Up @@ -131,7 +131,7 @@ class DatasetActionView extends React.PureComponent<Props, State> {
clearCache = async (dataset: APIMaybeUnimportedDataset) => {
this.setState({ isReloading: true });
await clearCache(dataset);
await this.props.updateDataset(dataset);
await this.props.reloadDataset(dataset);
Toast.success(
messages["dataset.clear_cache_success"]({
datasetName: dataset.name,
Expand Down
87 changes: 80 additions & 7 deletions frontend/javascripts/dashboard/advanced_dataset/dataset_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

import { Table, Tag } from "antd";
import * as React from "react";
import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons";
import { CheckCircleOutlined, CloseCircleOutlined, PlusOutlined } from "@ant-design/icons";
import _ from "lodash";
import { Link } from "react-router-dom";
import dice from "dice-coefficient";
import update from "immutability-helper";

import type { APITeam, APIMaybeUnimportedDataset, APIDatasetId } from "types/api_flow_types";
import EditableTextIcon from "oxalis/view/components/editable_text_icon";
import type {
APITeam,
APIMaybeUnimportedDataset,
APIDatasetId,
APIDataset,
} from "types/api_flow_types";
import { stringToColor, formatScale } from "libs/format_utils";
import type { DatasetFilteringMode } from "dashboard/dataset_view";
import DatasetAccessListView from "dashboard/advanced_dataset/dataset_access_list_view";
import DatasetActionView from "dashboard/advanced_dataset/dataset_action_view";
import FormattedDate from "components/formatted_date";
import { getDatasetExtentAsString } from "oxalis/model/accessors/dataset_accessor";
import { trackAction } from "oxalis/model/helpers/analytics";
import FixedExpandableTable from "components/fixed_expandable_table";
import * as Utils from "libs/utils";
import CategorizationLabel from "oxalis/view/components/categorization_label";

const { Column } = Table;

Expand All @@ -25,11 +34,14 @@ const useLruRank = true;
type Props = {
datasets: Array<APIMaybeUnimportedDataset>,
searchQuery: string,
searchTags: Array<string>,
isUserAdmin: boolean,
isUserTeamManager: boolean,
isUserDatasetManager: boolean,
datasetFilteringMode: DatasetFilteringMode,
updateDataset: (APIDatasetId, Array<APIMaybeUnimportedDataset>) => Promise<void>,
reloadDataset: (APIDatasetId, Array<APIMaybeUnimportedDataset>) => Promise<void>,
updateDataset: APIDataset => Promise<void>,
addTagToSearch: (tag: string) => void,
};

type State = {
Expand Down Expand Up @@ -68,8 +80,8 @@ class DatasetTable extends React.PureComponent<Props, State> {
});
};

updateSingleDataset = (datasetId: APIDatasetId): Promise<void> =>
this.props.updateDataset(datasetId, this.props.datasets);
reloadSingleDataset = (datasetId: APIDatasetId): Promise<void> =>
this.props.reloadDataset(datasetId, this.props.datasets);

getFilteredDatasets() {
const filterByMode = datasets => {
Expand All @@ -83,6 +95,12 @@ class DatasetTable extends React.PureComponent<Props, State> {
}
};

const filteredByTags = datasets =>
datasets.filter(dataset => {
const notIncludedTags = _.difference(this.props.searchTags, dataset.tags);
return notIncludedTags.length === 0;
});

const filterByQuery = datasets =>
Utils.filterWithSearchQueryAND<APIMaybeUnimportedDataset, "name" | "description">(
datasets,
Expand All @@ -95,9 +113,33 @@ class DatasetTable extends React.PureComponent<Props, State> {
? datasets
: datasets.filter(dataset => dataset.isActive && dataset.dataSource.dataLayers.length > 0);

return filterByQuery(filterByMode(filterByHasLayers(this.props.datasets)));
return filterByQuery(filteredByTags(filterByMode(filterByHasLayers(this.props.datasets))));
}

editTagFromDataset = (
dataset: APIMaybeUnimportedDataset,
shouldAddTag: boolean,
tag: string,
event: SyntheticInputEvent<>,
): void => {
event.stopPropagation(); // prevent the onClick event
if (!dataset.isActive) {
console.error(`Tags can only be added to active datasets. ${dataset.name} is not active.`);
return;
}
let updatedDataset = dataset;
if (shouldAddTag) {
if (!dataset.tags.includes(tag)) {
updatedDataset = update(dataset, { tags: { $push: [tag] } });
}
} else {
const newTags = _.without(dataset.tags, tag);
updatedDataset = update(dataset, { tags: { $set: newTags } });
}
trackAction("Edit dataset tag");
this.props.updateDataset(updatedDataset);
};

renderEmptyText() {
const maybeWarning =
this.props.datasetFilteringMode !== "showAllDatasets" ? (
Expand Down Expand Up @@ -188,6 +230,37 @@ class DatasetTable extends React.PureComponent<Props, State> {
</div>
)}
/>
<Column
title="Tags"
dataIndex="tags"
key="tags"
width={280}
sortOrder={sortedInfo.columnKey === "name" && sortedInfo.order}
render={(tags: Array<string>, dataset: APIMaybeUnimportedDataset) =>
dataset.isActive ? (
<div>
{tags.map(tag => (
<CategorizationLabel
tag={tag}
key={tag}
kind="datasets"
onClick={_.partial(this.props.addTagToSearch, tag)}
onClose={_.partial(this.editTagFromDataset, dataset, false, tag)}
closable
/>
))}
<EditableTextIcon
icon={<PlusOutlined />}
onChange={_.partial(this.editTagFromDataset, dataset, true)}
/>
</div>
) : (
<div style={{ color: "@disabled-color" }}>
Not tags available for inactive datasets.
</div>
)
}
/>
<Column
title="Voxel Size & Extent"
dataIndex="scale"
Expand Down Expand Up @@ -276,7 +349,7 @@ class DatasetTable extends React.PureComponent<Props, State> {
key="actions"
fixed="right"
render={(__, dataset: APIMaybeUnimportedDataset) => (
<DatasetActionView dataset={dataset} updateDataset={this.updateSingleDataset} />
<DatasetActionView dataset={dataset} reloadDataset={this.reloadSingleDataset} />
)}
/>
</FixedExpandableTable>
Expand Down
Loading

0 comments on commit deffe5a

Please sign in to comment.